かってぃのブログ

喫茶店を遊牧しながら勉強したり開発したりする大学院生のブログです。

katty0324

iPhoneアプリがメモリ不足で落ちる理由は「メモリが足りない → OSによってビュー強制終了 → 強制終了ビューのオブジェクトにアクセス → 応答不可 → アプリ強制終了」か?

on 2011-08-30 10:28:20

webから

iPhoneアプリは良く落ちる。

ランキングで上位に入ったり、話題になったりしているアプリでも落ちる時は落ちる。
なぜ落ちるか?

原因のほとんどは、

解放済みのオブジェクトにメッセージを送ってしまうこと

だと思います。


メモリ不足で落ちる?

「メモリ不足で落ちる」という言葉も良く使いますが、本質的には

  1. メモリが不足する
  2. 自動的にオブジェクトが解放される
  3. 解放されたオブジェクトにメッセージが送られる
  4. エラー

という流れで生じるものだと思います。

だから、この問題の解決策は、ふたつ。

  1. メモリを無駄に使わない(メモリリークを無くす)
  2. メモリ不足などによって解放されたオブジェクトにメッセージが送られないようにする

そのために気をつけるべきことなどを以下に列挙します。


alloc initしたら参照カウントが増えるので、かならずreleaseする。

非常に基本的なことですが、確保したものは解放します。

NSString *string = [[NSString alloc] initWithString:@"test"];
// ...処理...
[string release];


クラスメソッドで初期化した場合はreleaseしない。

次の例のようにクラスメソッドで初期化する場合があります。

この場合、autoreleaseの状態、つまり後ほど自動でreleaseが送られることになっているため、自分ではreleaseしません。

NSString *string = [NSString stringWithString:@"test"];
// ...処理...


CF〜Create()で作られたオブジェクトはCFRelease()で開放する。

CF〜Create()という関数で作られたオブジェクトも必ず解放する必要があります。

CFDataRef dataref = CFDataCreate(NULL, buffer, CFDataGetLength(data));
// ...処理...
CFRelease(dataref);


viewDidUnloadはいつ呼ばれる?

viewDidUnloadというメソッド名を見ると、まるでビューが閉じる時に呼ばれそうなものですが、これは勘違い。

一度も呼ばれることなくビューが開いて閉じることも多々あります。


ではいつ呼ばれるかといえば、低メモリ状態になった時です。

低メモリ時、最前面で表示されているもの以外のビューコントローラにはviewDidUnloadが呼ばれます。


viewDidLoadで確保した変数はviewDidUnloadでreleaseする。

viewDidUnloadされたビューコントローラは、画面が空っぽになってしまいます。

まだ使っている最中のビューコントローラが空になっても、問題がないのは、次に表示されるときに再びviewDidLoadが呼ばれて画面を作り直すためです。

もし、1回目のviewDidLoadで確保したオブジェクトをviewDidUnloadで解放していないと、2回目のviewDidLoadをするときにメモリリークを起こします。

だから、viewDidLoadで確保したオブジェクトはviewDidUnloadで解放します。


initとviewDidLoadで確保したメモリはdeallocでreleaseする。

上でも書いたように処理が終わってビューコントローラが閉じる時には、viewDidLoadは呼ばれません。

deallocだけが呼ばれます。

したがって、initで確保したオブジェクトはもちろん、viewDidLoadで確保したものも解放する必要があります。


didReceiveMemoryWarningでオブジェクトを解放する時はnilを代入する。

低メモリ状態になると、確保されている全てのビューコントローラにdidRecieveMemoryWarningが呼ばれます。

viewDidUnloadは、ビューコントローラが閉じた状態になると、もう呼ばれません。

しかし、didReceiveMemoryWarningはメモリが足りなくなるたびに何度でも呼ばれます。


ここで注意しなければならないことは、1回目のdidReceiveMemoryWarningで解放されたオブジェクトに、2回目のdidReceiveMemoryWarningで再びreleaseが送られるとクラッシュします。

そこで、didReceiveMemoryWarning内で解放したオブジェクトにはnilを代入して、二度目の解放処理が行われないようにします。

(nilはreleaseを受け取ってもクラッシュしないためです)


というかオブジェクトを解放する時はnilを代入する。

解放後にメッセージが送られる可能性があるオブジェクトを解放する時は、releaseだけでなくnilの代入が必要です。

そうでない場合でも、nilを代入して悪いことはないです。

[obj release];
obj = nil;


プロパティとして定義されているものは、nilを代入して解放する。

プロパティとして定義されている変数はセッターにreleaseの処理が含まれているので、nilを代入するだけで済みます。

self.obj = nil;

この解放手段は、プロパティがassignかretainかcopyか考える必要がなくて一石二鳥です。

http://iphone-dev.g.hatena.ne.jp/tokorom/20100314/1268591111


ただし、次のように書いてしまうとセッターが働かず解放できないので注意。

obj = nil


マルチスレッドの場合は、全てのスレッドでNSAutoreleasePoolを作成する。

NSAutoreleasePoolはスレッド内でのみ有効なので、新たにスレッドを立てる場合はNSAutoreleasePoolを作成する必要があります。


ループ内など大量のオブジェクトを確保するところでは、局所的にNSAutoreleasePoolを作成する。

ループ内でオブジェクトを確保するような処理では、大量のメモリを消費するので、NSAutoreleasePoolを局所的に作成するなどして、ループが1周するたびにメモリを解放するようにしてやります。


UIScrollViewでaddSubviewをする場合は、画面外に出たviewをremoveFromSuperviewする。

UITableViewでは画面外に出たセルを自動的に再利用する機構がありますが、UIScrollViewにはありません。

したがって、そのような機構を自作するなどして画面外に出たviewを閉じることでメモリ使用量を削減します。


メモリ使用量の取得方法

プロファイラなどを使えば見えるが、プログラム中で値として取得したい場合

http://d.hatena.ne.jp/kimada/20090405/1238914098#20090405f1


メモリリークを調べる。

Xcode4をRunではなくAnalyzeで実行することで、本質的にメモリリークになる部分を全て指摘してくれます。便利。

これを全てつぶしても、動的に生じるメモリリークは残るので、更にProfileで実行してメモリリークをプロファイルします。

これでひとつずつ確認して原因を潰していく作業が待っています。


メモリ警告シミュレーションの仕方

iOSシミュレータのメニューから、メモリ警告シミュレーションを実行できる。

これでメモリ不足の警告を出し、つまり背面のビューコントローラをUnloadしたりして、メモリリークが起きたり、解放されたオブジェクトにメッセージが送られるようなプログラムになっていないかを確認します。


おわりに

以上で終わりです。

勉強しながらまとめたものですので、もし誤っているところがありましたら、お知らせください。


参考URL

http://konton.ninpou.jp/program/cocoa/memory.html
http://iphone-dev.g.hatena.ne.jp/tokorom/20100314/1268591111
http://hamasyou.com/blog/archives/000384
http://labs.torques.jp/2010/11/07/1495/
http://safx-dev.blogspot.com/2010/10/viewcontroller.html
http://d.hatena.ne.jp/sakusan_net/20110226/1298727973
http://journal.mycom.co.jp/column/objc/104/index.html
http://hamasyou.com/blog/archives/000383
http://d.hatena.ne.jp/glass-_-onion/20110218/1297963007
http://cocoadays.blogspot.com/2010/10/nsautoreleasepool.html

by katty0324 on 2011-09-05 23:36:05

コメント(0)

katty0324

UIImageからCGImageを取り出すと、左に90度傾く現象ではまってる。iPhoneを横にして写真撮ったときは90度傾けると正しい方向になるんだけど、iPhoneを縦にして撮っても強制的に傾く。うーん。

on 2011-06-08 02:20:11

webから

iPhoneのカメラで撮った写真をトリミングしたい

UIImagePickerControllerを使って、写真を撮影し、その写真から正方形の領域を切り出したいと考えていました。
軽く調べたら以下のようなコードでできそうだということが分かりました。

- (void)imagePickerController:(UIImagePickerController*)picker
        didFinishPickingImage:(UIImage*)image
                  editingInfo:(NSDictionary*)editingInfo
{
    UIImage* imageOriginal = [editingInfo objectForKey:UIImagePickerControllerOriginalImage];
    CGRect cropRect;
    [[editingInfo objectForKey:UIImagePickerControllerCropRect] getValue:&cropRect];
    CGImageRef imageRef = CGImageCreateWithImageInRect(imageOriginal.CGImage, cropRect);
    UIImage *imageCropped =[UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);   
}

簡単に要約します。
UIImagePickerControllerのeditingInfoから、元の写真と切り出す領域の情報を取得します。
写真を一度CGImageRefに変換すると画像の切り出しができます。
切り出した後、再びUIImageに戻せば完了です。

カメラで撮影した写真が90度左に回転する

ところが、カメラで撮影した写真がなぜか90度左に回転してしまいました。
何かがおかしい。

imageOrientationが関係している?

色々考えた結果、どうやら撮影するときのiPhoneの向きが問題になりそうだということが分かりました。
iPhoneは縦に構えたり、横に構えたりできます。
どの向きになっているかは自動で判別されて、imageOriginal.imageOrientationなどに記録されています。

だから、本来であれば、勝手に正しい向きにしてくれます。
むしろ変な方向に回転させたりはしないはずです。

・・・はずなんですが、どうやらUIImageのCGImageプロパティはこれを判別していないように思います。

一方、カメラの向きを正しく判別して処理を行うメソッドもあります。
drawInRectです。

解決策

そこで、解決策。
drawInRectを使って、元画像を一度書きなおしてみよう。

- (void)imagePickerController:(UIImagePickerController*)picker
        didFinishPickingImage:(UIImage*)image
                  editingInfo:(NSDictionary*)editingInfo
{
    UIImage* imageOriginal = [editingInfo objectForKey:UIImagePickerControllerOriginalImage];

    // おまじない始まり
    UIGraphicsBeginImageContext(imageOriginal.size); 
    [imageOriginal drawInRect:CGRectMake(0, 0, imageOriginal.size.width, imageOriginal.size.height)]; 
    imageOriginal = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext();
    // おまじない終わり

    CGRect cropRect;
    [[editingInfo objectForKey:UIImagePickerControllerCropRect] getValue:&cropRect];
    CGImageRef imageRef = CGImageCreateWithImageInRect(imageOriginal.CGImage, cropRect);
    UIImage *imageCropped =[UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);   
}

正しく動きました。
果たしてこれで良いのかは微妙だけど、とりあえず。

久々にハマりこんだので、記録。

by katty0324 on 2011-06-08 03:11:23

コメント(1)