メモリーの管理は、ネイティブ iOS アプリを開発する誰にとっても重要であり、不可欠なことです。このことは、Apple の新しいプログラミング言語である Swift を使っているとしても、変わりはありません。私は IBM の Mobile Innovation Lab でメモリーに関するいくつかの問題に直面し、それらの問題を解決してきました。
まずは簡単な背景情報から
Objective-C にはこれまで一度も、Java のような言語に実装されているようなガーベッジ・コレクションが実装されたことはありません。開発者が Objective-C を使用するようになった当時、メモリーの管理は、参照カウント・システムを使用して手作業で行うしかありませんでした。その作業はほとんどのところ、オブジェクトを割り当てるときや割り当てを解除するときに、キーワード retain
、release
、autorelease
を追加するというものでした。
Apple が数年前に Xcode 4.2でリリースした ARC (Automatic Reference Counting: 自動参照カウント) は、手作業で参照カウントを行うという負担を開発者から取り除くことに成功しました。その方法は、単に開発者がメモリーを管理するために行うすべての作業を代行するというものです。ただし ARC を使用したとしても、メモリーの問題が発生する可能性はあります。そこで、このチュートリアルでは ARC を使用する際に避けなければならない落とし穴と、アプリのメモリーの問題を検出およびデバッグするための手法について取り上げます。
読む:Apple の開発者向けサイト (古い手法について詳しく学んでください)
一般的なメモリーの問題
保持サイクル
私がつい最近取り組んだ Objective-C アプリでは、保持サイクルに対する修正によって、これまで私が行ったあらゆるメモリー・バグの修正の中で最も大きな改善がメモリーに見られました。保持サイクルとは基本的に、2 つのオブジェクトが互いを保持している状態のことです。この状態は、オブジェクトのオーナーシップの標準ルールに反し、両方のオブジェクトに他方を解放する権限が与えられないため、メモリー・リークが発生することになります (図中の数字は保持カウントです)。
上記の図では、2 つのオブジェクトがオブジェクト B を参照しています。この場合、オブジェクト A が解放されても、オブジェクト C がオブジェクト B を参照しているため、オブジェクト B は存続します。この時点で、オブジェクト B またはオブジェクト C を解放するオブジェクトは残されていません。こうした問題があることから、ARC によって保持 (retain) や解放 (release) の呼び出しに関する私たちの懸念は取り除かれても、私たちはなお、保持サイクルと同様に強参照と弱参照についても考える必要があります。また、Swift でも ARC を使用するので、保持サイクルは Swift でも起こり得ます。
読む:ARC に関する Swift のドキュメント (サイクルの例)
私の対処法
私が取り組んでいたアプリは、大量の画像処理を行うものでした。iPhone 4S で UI 自動テストを実行すると、約 5 分後にアプリが異常終了します。このことから、私のチームはメモリーの問題があることに気付きました。最終的に私が問題の原因として突き止めたのは、次のようなコードです。
@property (nonatomic) id<MyDelegate> delegate;
私たちはビュー・コントローラーの 1 つで、この delegate に self を設定していました。一見それで問題ないように見えましたが、後になって、参照オプションを指定しないプロパティーには暗黙的に strong 属性が設定されることに気づきました。つまり、このプロパティーがビュー・コントローラーへの強参照を保持していたため、このビュー・コントローラーの設定は解除されずに、メモリー使用量が急激に上昇していたというわけです。このことが明らかになった後、すぐにこのプロパティーを weak 属性にしました。
@property (weak) id<MyDelegate> delegate;
このプロパティーに weak 属性を追加してからは、アプリは iPhone 4S で 45 分間実行されるようになりました。たった 1 行のコードを変更しただけで、実行時間が 9 倍に改善されたわけです。
不要なキャッシング
私たちのアプリでは、多種多様な画像をダウンロードして、それらの画像をキャッシングしていましたが、これは理想的なキャッシングの使用方法ではありません。キャッシングが最適なのは、頻繁にアクセスされるオブジェクトを格納する場合です。このアプリでキャッシングする画像は、頻繁にアクセスするものではありません。NSCache やその他のキャッシング・ライブラリーは、メモリーが不足してくると、キャッシュを部分的に破棄するものの、詳細な制御を必要とする開発者もいます。
多くの Apple フレームワーク (Foundation、UIKit、CoreLocation) は依然として Objective-C で作成されているため、キャッシングの問題は Objective-C を使用する場合と同様、Swift を使用した場合でも発生する可能性があります。そうは言っても、キャッシング・システムを使用することにメリットがあるのか、それともそれによって不要なメモリー使用量が増えていくことになるのかを判断するのは開発者の責任です。
私の対処法
私たちが取り組んでいたアプリでは、キャッシュの制御を強化する必要がありました。そのため、キャッシング・ライブラリーを使用して、キャッシュへのテーブル・ビューの割り当てが解除されるまで (つまり、私たちがキャッシュをクリアするまで)、そのビューに写真をキャッシングすることにしました。この方法により、テーブル・ビューのスクロールが円滑になっただけでなく、画像が必要以上にメモリーに保持されることがなくなりました。
ARC が C で処理する内容が不明なこと
私たちの Objective-C アプリでは、ARC が何を処理しているのかを知らなくても大した問題ではありませんでしたが、それでも私は、C コードを適切に処理するようにしておきました。基本的に、ARC は C iOS ライブラリー (Core Graphics、Core Text など) や自身が作成した C コードには適用されません。Core Graphics では、開発者自身が C の関数を呼び出して変数を解放する必要があります。
CGImageRef imageRef = CGImageCreateWithImageInRect([self.cropView.image CGImage], CropRect); cropped = [UIImage imageWithCGImage:imageRef]; CGImageRelease(imageRef);
私たちは上記のコードを使用して、アプリの中で実際に画像の切り抜きを行いましたが、Core Graphics 画像参照を作成した後、処理が完了するとすぐにその画像参照を解放していることに注意してください。
Swift に関する注記: このケースは、Swift での処理の仕組みと直接的な相関はありません。Core Graphics の場合、厳密には開発者が手作業で CGImageRefs を解放する必要はなく、実際には ARC が開発者に代わって Swift でこの処理を行います。ただし、これは Swift のすべての C ライブラリーに当てはまるわけではないので、決めてかかる前に必ずマニュアルを調べてください。実際のところ、Swift には便利な新機能、構文に関する多数の変更が加えられていますが、Objective-C との緊密なつながりがあります。
メモリーの問題をデバッグして回避する方法
dealloc
メソッドをオーバーライドする
ビュー・コントローラーに対する dealloc
メソッドをオーバーライドすると、ビュー・コントローラーの割り当てが、解除されるべきときに確実に解除されるようになります。以下の Objective-C の例に示すように、このメソッドをオーバーライドするのは簡単です。
-(void)dealloc { NSLog(@"viewcontroller is being deallocated"); }
アプリで保持サイクルを見つけるのに役立ったのは、この手法です。この手法により、特定のビュー・コントローラーが割り当て解除されないまま、そのビュー・コントローラーのインスタンスが追加で割り当てられたために大々的なメモリー・リークが発生していたことが明らかになりました。これは、私がメモリーの問題をデバッグする際に最初に適用している戦略の 1 つです。
autoreleasepool を手作業で作成する
autoreleasepool の作成に関する Apple の説明には、通常は開発者自身が作成する必要はないと書かれています。ただし場合によっては、開発者が autoreleasepool を作成すると、アプリのピーク・メモリー・フットプリントを削減するのに役立つことがあります。autoreleasepool が役立つ理由は、開発者が作成したオブジェクトが何であれ、システムがそれを解放するまで待機するのではなく、それらのオブジェクトをブロック (Swift の場合はクロージャー) の最後で解放するようシステムに指示できるためです。この仕組みを理解するには、以下の例が役立ちます。
for (int i=0; i<5000; i++) { @autoreleasepool { NSNumber *num = [NSNumber numberWithInt:i]; [num performOperationOnNumber]; } }
数値を扱っているだけであれば、メモリー使用量が大きくなりすぎることはないはずですが、ループを繰り返し処理している間はオブジェクトが解放されても、最後にすべてのオブジェクトが解放されるとは限りません。UIImages のようなオブジェクトを扱う場合や、base64 NSStrings のようなオブジェクトを扱う場合であっても、これらのオブジェクトのいずれかが解放される前に、メモリー使用量がかなり大きく可能性があります。私たちのアプリではいくつかの autoreleasepool を使用したものの、一度に大量のメモリーを使用するデータを扱ってはいなかったので、それによって大きな違いがあったとは思いません。.
Swift に関する注記: Swift でのメモリー管理で変更されている点はほとんどありませんが、autoreleasepool の使用に関しては変更点があります。Objective-C も使用している Swift プロジェクトで作業している場合は、autoreleasepool を使用しなければならない可能性や、これを使用できる可能性が考えられます。純粋な Swift プロジェクトで作業しているとしたら、autoreleasepool クロージャーを作成する必要が生じることはないはずです。ちなみに、Swift での autoreleasepool の構文は同じですが、先頭に @ 記号は付きません。
見る:Improving Your App with Instruments (WWDC 2014 カンファレンスより)
問題が潜在している可能性がある領域を切り分ける
コードをデバッグしていてもメモリーの問題の原因を特定できない場合は、疑いのあるコードを切り分けると、原因の特定に役立つことがあります。例えば、特定のメソッドを何度も呼び出すようにコードを一時的に変更し、メモリーの割り当てと割り当て解除の処理を行った結果がどのようになるかを確認するという方法があります。また、ナビゲーション・ベースのアプリでは、問題が潜在しているメソッドを使用して継続的にビュー・コントローラーのプッシュとポップをすることで、ポップのたびに解放されることが求められるオブジェクトが解放されていることを確認するという方法も有用です。
私たちのアプリでは一時、画像切り抜きビューが、アプリのメモリー使用量が大きくなる問題の原因になっているのかどうかを判断できなかったため、ビュー・コンロトーラー全体を新しい Xcode プロジェクト内に切り分けました。Xcode Instruments でプロジェクトを分析してみると、このビュー・コントローラーが問題であるという確信に至る結果は何も出ませんでした。私はこの手法をデバッグ作業全体にわたって実行しました。この手法はメモリーの問題が存在しない領域を明らかにする上で有効であったため、細部にわたるデバッグに要する時間が大幅に削減される結果になりました。
Xcode Instruments を使用したデバッグ
私からの最後のヒントは、デバッグ作業に Xcode Instruments を使用することです。Instruments は、Xcode ツールセットに含まれているアプリケーションの 1 つで、作成したアプリをデバッグ、テスト、あるいは最適化する上で必要になる可能性があるすべての機能を備えています。Instruments アプリケーションには、Allocations、Leaks、Automation、Time Profiler をはじめ、多種多様な分析ツールが含まれています。このチュートリアルでは、Xcode 6 Instruments の Allocations ツールに焦点を当てます。このツールは、私がメモリーの問題をデバッグするときに最もよく使用したツールであり、今すぐまたは将来、皆さんの役に立つと確信しています。
以降のステップを開始する前に、Spotlight 検索から、または以下のように Xcode から直接、Instruments を開くことができます。
ステップ 1. Allocations 分析ツールを選択する
- Allocations のプロファイル・テンプレートを選択します。
- Instruments のメイン・インターフェースで、「VM Tracker」が表示されている場合、この特定のツールが必要になることはないので、Delete キーを押してこのツールを削除します。
右上にあるプラス記号のボタンをクリックして、他の種類のテスト用のツールを追加することもできますが、それらのツールについては、このチュートリアルでは取り上げません。
ステップ 2. Instruments の設定を構成する
分析を実行する前に、いくつか必要な作業があります。まず、アプリをインストールする iOS 端末を接続する必要があります。これは物理端末でなければなりません。iOS Simulator はシミュレーターに過ぎないので、アプリのメモリー使用量や、メモリーが逼迫した状態でのアプリの動作を正確に表さない可能性があるからです。
ターゲットを選択するには、上のほうにある「My Computer (マイコンピュータ)」をクリックし、自分の端末の上にカーソルを重ねて表示されるサブメニューから分析対象のアプリを選択します。
次に表示されるパネルで、表示する割り当てのタイプに関する設定を変更することができます。「Created & Persistent (作成して維持)」項目が選択された状態にする以外に前もって行わなければならない設定はありません。
オプション: Allocations ビューをズームイン表示するには、左側に示されている「Allocations」をクリックして、「Command+」を押します。
ステップ 3. 記録ボタンを押してツールを実行する
左上にある「Record (記録)」ボタンを押すと、端末上でアプリが起動し、Instruments が割り当て量のグラフ作成を開始します。ここで必要となる唯一の作業は、アプリを実行しながら問題が考えられるエリアに注目し、メモリーの割り当て量が、割り当てを解除される量を上回っているかどうかを調べることです。この作業は何度も繰り返さなければならないことになる場合がありますが、その苦労は後で報われます。
以下のような出力が表示されるはずです。
一度、アプリを通しで実行し、メモリーが安定した状態にすることをお勧めします。これが、メモリー使用量の増加を見極めるのに有効なベースラインになります。テストするのに十分なデータが揃ったら、左上にある停止ボタンを押します。
ステップ 4. 分析する
分析する際に私がまず始めに行うことは、検査範囲を設定し、ベースラインで維持される合計バイト数を測定することです。この維持バイト数 (Persistent Bytes) は、割り当て量サマリー (Allocation Summary) の直下に示されます。
実際の検査範囲を設定するには、キーボード・ショートカット「command <」を使用して検査範囲の開始点を設定し、「command >」を使用して検査範囲の終了点を設定します。私たちのアプリでのベースラインは約 20 MB でした。
- 次に、もう一度アプリを通しで実行し、実行前の状態に戻った時点へと検査範囲の終了点を移動します。以下の図からわかるように、メモリー使用量はアプリの実行前とほぼ同じです。そのため、この作業を数回繰り返して、メモリー使用量がベースラインに戻ることを確認できれば、大きなメモリーの問題はないという前提に立つことができます。.
このデータを分析するには他の方法もあります。ここでそれらの方法を取り上げることはしませんが、データを表示する方法や分析する方法に関するドロップダウン・メニューがあることを覚えておいてください。
ステップ 5: 世代のマークを付ける
検査範囲を毎回設定するのが面倒だという場合には、「Mark Generation (世代のマーク)」という機能を使用することもできます。Instruments の右パネルには、そのためのボタンがあります。
このボタンは、Instruments の時間軸上で検査ラインがあるポイントにマークを付けます。その目的は、前のマーク以降のすべての割り当て処理を記録すること、あるいは初めてマークを付ける場合は、始めからそのマークまでのすべての割り当て処理を記録することです。世代のマーキングは、割り当てツールの実行中、または以下の例のようにアプリの実行を停止した後に行うことができます。
もう一度アプリを実行すると、上図に示すように、各世代をマークする赤い小さなフラグが時間軸上に示されます。一番下には世代のビューが表示されるので、そのビューで、すべてのデータおよび世代間でのデータの変化を確認することができます。このようにすると、アプリをナビゲートして画面を切り替えながら、メモリー使用量の増加を簡単に確認することができます。場合によっては、これは非常に有用な方法になりますが、セッションを記録した後に手作業で検査範囲を設定したほうが簡単なこともあります。
ステップ 6. スタック・トレースを調査する
最後に取り上げる作業は、スタック・トレースの調査です。スタック・トレースを調査するには、まず、検査範囲を設定してすべての割り当て処理を強調表示します。そして、右パネルで「Created & Persistent (作成して維持)」項目が選択されていることを確認した上で、統計ビューを調べる必要があります。統計ビューでは、「Persistent Bytes (維持バイト数)」が高い値から低い値の順にソートされていることを確認してください。このビューには多数の割り当て処理が表示され、その多くはシステム割り当て処理であるため、その内容を理解するのは難しい場合があります。
詳細を探る
- 割り当て量が最大になっている処理を見つけて、その右向き矢印をクリックします。ほとんどの場合、割り当て量が最大の処理をクリックすると、その処理内で複数の割り当て処理が行われています。その大部分は、開発者にとって意味がありません。
- 矢印をクリックした後に他の割り当て処理を強調表示して、右パネルで拡張詳細を調べます。最終的には、太字のテキストが見つかるはずです。そのテキストは、プロジェクト内で問題が存在する可能性がある実際のコードにリンクしています。
- スタック・トレースで太字の項目のいずれかをダブルクリックすると、実際のコードが表示されます (自分が所有するアプリで Allocations ツールを実行したことが前提となります)。
- このビューで役立つ点はたくさんあります。その 1 つは、右側に表示される黄色のタグに、それぞれのメソッド呼び出しが使用しているメモリーの量が示されることです。すべてのアプリはそれぞれに異なるため、強調表示されたメソッドが問題なのか、最適化できるものなのか、アプリで避けられないメモリー使用なのかは、開発者が判断しなければなりません。
- 私のケースで言うと、UIColor 変数はアプリ全体で維持されて使用されるため、アプリの存続期間をとおしてメモリーの使用が容認されます。
まとめ
Xcode の更新や、Swift のような新しい開発によって、事態は常に変化しています。iOS の開発でガーベッジ・コレクターが実装されない間は、メモリー・リークを回避するよう注意しなければなりません。しかも、できれば開発しているアプリがほとんど完成した時点で注意を払うのではなく、開発プロセス期間から注意を払うようにしてください。このチュートリアルで提供したヒントが、皆さんがメモリーの問題の多くを解消してメモリー効率の高いアプリを作成する上で役立つことを願っています。
読む:IBM Mobile Data for Bluemix サービスを利用して iOS アプリを作成する
読む:iOS アプリを Objective-C から Swift に移植する
関連トピック:iOS アプリの開発SwiftXcode