なかなかに曲者のARC。ガベージコレクションではないというのが曲者たる所以で、安心しきっていたら、気づかぬ間にメモリが蓄積され、いずれはメモリリークに発展する。
ましてや、Storyboardを使って循環する遷移を作ってしまうと、3週目や4週目に突然落ちたりするので原因がよくわからない。
何度泣かされ、何度解決方法を探しさまよったことか。そして今日ついに、ネックであったメモリに読み込んだ画像の解放に成功した。ふぅ。
メモリリークには、色々なパターンが存在するが、最近のハードウェアはとにかく高解像度。手元の Xperia Z1 はなんと、1920px * 1080px の FullHD。 iPhoneだって、 1136px * 960px と HDに近いサイズ。私が初めてまともにゲーム開発に携わったハードの解像度は 224 * 144 ドット。 まさかの単位の呼び方が違うというおまけ付き。
こんなに、解像度が大きいと画像も必然的に大きくなり、それをメモリに置くとなると、メモリ消費量も尋常ではありません。こんな奴らが開放されないで居座り続けると、メモリがいくらあっても足りません。
これから、解像度が4K、8Kに移っていくのかと思うとフラットデザインをしっかり学ぶ必要があるのが身にしみてわかります。
さて、本題です。ARCはretainカウンターの管理をちょっと楽にしてくれるだけなので、放置しておけば何でもかんでも解放してくれるわけではありません。特にガッツリとした強参照の時にはねちっこくメモリを保持し続けます。
UIImageView周りのメモリ管理のお話を追いかけて行くと、imageNamed はキャッシュされるから使うシーンをあやまるな!!なーんてのも出てきて、そんじゃぁ、initWithContentsOfFile でやるかと意気込むと、どんどん、溜まってい待ってますが大丈夫ですかぁ?と、教えてくれずに突然ぷっつり。いやいや、Xcode5から、メモリ消費量がデバッグビルドでも出るから今まで以上に緊張感が持てるようになりました。
さて本題に移りましょう。このケースは、initWithContentsOfFileを使っている場合を想定しています。それでは、れっつごー!!
画像をメモリ上に置くための UIImage の場合は、UIImageView.image と結びつけているためにまずは切り離してやる必要があります。私の場合は切り離す処理をクラスメソッドにして、ユーティリティとして使っています。
1 2 3 4 5 6 7 |
+ (void)releaseUIImageView:(UIImageView*)uiimgv { if(uiimgv != nil){ uiimgv.image = nil; uiimgv.layer.sublayers = nil; uiimgv = nil; } } |
しかし、残念ながらこれだけでは消えてくれないのが Objective-C の悲しいところです。この後に UIImageView を addSubViewした親のViewから消す必要があります。例えば、ViewController に addSubView した場合はこんな感じ。
1 |
[self.view removeFromSuperview]; |
これで、AllocationsのStill Living(確保されているメモリーのリスト)から抹殺することできました。Instrumentsは本当に便利です。最近使い方がわかってきたので、後日記事がかければいいなぁ。
ということで、最後にまとめのプログラムを一式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// // Created by Azuma Shimpei on 2014/01/08. // right. Public Domain // // Class DokidokiViewはUIViewを継承しています。 #import "DokiDOkiView.h" UIImage* dokidokiImg; @implementation DokidokiView - (void)loadImgae { dokidokiImg = [SAUtility pathForResource:@"dokidoki.png"]; } - (void)registSubView { LOG_METHOD; UIImageView *local = [[UIImageView alloc] initWithImage:dokidokiImg]; local.frame = CGRectMake(0, 0, 240, 275); local.tag = 4000; local.userInteractionEnabled =NO; [self addSubview:local]; } - (void)releaseSubView { UIImageView* uiimgv = (UIImageView*)[self viewWithTag:4000]; [SAUtility releaseUIImageView:uiimgv]; [self removeFromSuperview]; } @end // // Created by Azuma Shimpei on 2014/01/08. // right. Public Domain // #import "SAUtility.h" @implementation SAUtility + (UIImage*)pathForResource:(NSString*)fname { NSString *imagePath = [[NSBundle mainBundle] pathForResource:fname ofType:nil]; return [[UIImage alloc] initWithContentsOfFile:imagePath]; } + (void)releaseUIImageView:(UIImageView*)uiimgv { if(uiimgv != nil){ uiimgv.image = nil; uiimgv.layer.sublayers = nil; uiimgv = nil; } } @end |
私の場合は
- loadImgae は viewDidLoad
- registSubView は viewWillAppear:
- releaseSubView は viewDidDisappear:
で、呼び出すようにしています。
いきなり、UIViewを removeFromSuperview するだけでなく UIImageView を1つずつ nil のは面倒ですが仕方ないです。「入り口は1個出口は1個」「開けたら閉める」、いまさらながらの教訓です。
みなさんも快適なアプリ開発ライフをお過ごしください。
おわり
コメント
[…] こちらのサイトで詳しく説明されておりますが、どうやら […]