2010年5月18日火曜日

iPhone開発でメモリ使用量を把握しよう! 2

開発部の三森です。前回の続きです。
メモリ使用量を見るためにObjective-Cのランタイムapiを使ってみることを考えます。
Objective-Cで書かれたプログラムは、コンパイルされたプログラムだけでなく、ランタイムが必要になります。
このランタイムがオブジェクトの生成、メソッドの使用などをプログラムの実行時に動的に割り当てるようになっています。
そしてランタイムapiを使えば、オブジェクトのもつメソッドを自由に入れ替えたり書き換えたりできるのです。
どんなapiがあるかはAppleのドキュメントに詳しく書いてあります。

ランタイムapiを使うには、まず

#import <objc/runtime.h>

でヘッダを読み込みます。

オブジェクトのメモリ確保はNSObjectの持つ"+alloc"セレクターで行われるので、
これを新しいものに上書きしてデバッグ情報を得ることを目標にします。

今回はMemoryCheckSampleというiPhoneアプリのプロジェクトを作り、
MemoryCheckSampleAppDelegate.mというファイルのみを変更しました。
以下がそのファイルに新しく足した部分のソースコードです。


#import "MemoryCheckSampleAppDelegate.h"
#import <objc/runtime.h>

unsigned long GlobalMemoryCounter = 0;

@implementation MemoryCheckSampleAppDelegate

+ (void)load {
id meta = [NSObject class]->isa;
IMP oldAllocImp = class_getMethodImplementation(meta, @selector(alloc));
IMP newAllocImp = class_getMethodImplementation(self->isa, @selector(newAlloc));
Method m_oldAlloc = class_getClassMethod(meta, @selector(alloc));
Method m_newAlloc = class_getClassMethod(self, @selector(newAlloc:));
class_addMethod(meta, @selector(oldAlloc), oldAllocImp, method_getTypeEncoding(m_oldAlloc));
class_replaceMethod(meta, @selector(alloc), newAllocImp, method_getTypeEncoding(m_newAlloc));
}

+ (id)newAlloc {
id obj = [self oldAlloc];
NSLog(@"%@ allocated %zd", self, malloc_size(obj));
GlobalMemoryCounter += malloc_size(obj);
return obj;
}


+loadというのは初めにこのクラスがランタイムにロードされる時に呼び出されるセレクターです。
ここでNSObjectクラスの+allocの実装を+newAllocに入れ替えるということをしています。
+loadの一行目から説明すると

1. NSObjectのメタクラスがallocセレクターを持つので、それをmetaと呼びます。
2. 今までのallocセレクターで呼ばれるメソッドの実装を取り出し、oldAllocImpと呼びます。
3. このクラスのもつnewAllocセレクターの実装をnewAllocImpと呼びます。
4. 5. alloc, newAllocセレクターに対応するメソッドを取り出します(メソッドの型情報を取るために必要)
6. metaに今までのallocと同じ機能をもつoldAllocセレクターを追加します。
7. metaのallocセレクターで呼ばれるメソッドをnewAllocImpにします。

ということをやっています。今や+allocで呼ばれるようになった+newAllocの中でやっているのことは、
allocした後にそのオブジェクトのメモリーサイズをカウントするということです。
カウンターにはGlobalMemoryCounterというグローバル変数を使っています。試しに


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[window makeKeyAndVisible];
NSLog(@"Allocated size : %ld", GlobalMemoryCounter);
}

と書いてみると、デバッグコンソールは

NSAutoreleasePool allocated 32
NSAutoreleasePool allocated 32
__NSPlaceholderArray allocated 16
__NSPlaceholderArray allocated 16
NSMutableArray allocated 16
WTFMainThreadCaller allocated 16
NSThread allocated 64
_NSThreadData allocated 96
NSMutableDictionary allocated 16
NSDate allocated 16

(... 大量のログ ...)

Allocated size : 4640

のようになり、ここまでで色々なオブジェクトを生成していることが分かります。

前回お話した

[UITableView alloc];
[[UITableView alloc] init];

の違いも見てみましょう。

見やすさのために以下のようにマクロを用意してLOG_ALLOC()マクロだけで見たい処理を書けるようにします。
LOG_ALLOC()を呼び出す度にGlobalMemoryCounter = 0でリセットします。

#define LOG_VOID NSLog(@"\n");
#define LOG_BAR1 NSLog(@"======================================");
#define LOG_BAR2 NSLog(@"--------------------------------------");

#define LOG_ALLOC(expr) LOG_VOID LOG_BAR1 NSLog(@"%s;", #expr); GlobalMemoryCounter = 0; LOG_BAR2 expr; \
LOG_BAR2 NSLog(@"Allocated size : %ld", GlobalMemoryCounter); LOG_BAR1 LOG_VOID

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[window makeKeyAndVisible];
NSLog(@"Allocated size : %ld", GlobalMemoryCounter);

LOG_ALLOC([UITableView alloc]);
LOG_ALLOC([[UITableView alloc] init]);
}


ログの結果はそれぞれ

======================================
[UITableView alloc];
--------------------------------------
UITableView allocated 1024
--------------------------------------
Allocated size : 1024
======================================

======================================
[[UITableView alloc] init];
--------------------------------------
UITableView allocated 1024
CALayer allocated 48
UISwipeGestureRecognizer allocated 128

...

NSMutableArray allocated 16
UICachedDeviceRGBColor allocated 32
UICachedDeviceRGBColor allocated 32
--------------------------------------
Allocated size : 1696
======================================

これで違いが分かるようになりました。

0 件のコメント:

コメントを投稿