iPhoneアプリ開発 性能解析ツール

作成日:2020/12/21

お役立ちコラム

iPhoneアプリ開発 性能解析ツール

前回(第3回)は「 iPhoneアプリ開発 リビジョン管理 」と題して、Xcode と Git を使ってソースコードのリビジョン管理をする方法、および GitHub をハブにして開発チームでソースコードを共有する方法を解説しました。
今回は、「 iPhoneアプリ開発 性能解析ツール 」と題して Xcode に付属する Instruments というランタイムの性能解析のツール群の中から、アプリの起動速度、処理速度、メモリ効率の改善に役立つツールを紹介します。

デバッガーでデバッグし、自動・手動のテストを行い、アプリが仕様通りに動作することを確認しました。
さて App Store で公開しても良いでしょうか?
問題ないかもしれませんが、その前に Instruments を用いてアプリが軽快に動作するかを確認した方が良いでしょう。

ユーザーのアプリに対する第一印象は起動速度で決まります。
何カ月も動き続けるサーバー側ソフトウエアではありません。
iPhone のアプリは日に何度も起動されます。
その度に何秒も待たされれば印象が悪くなります。
次に操作に対するレスポンスです。
操作のたびに何秒も待たされたら、これも印象が悪くなります。
またCPU時間を使いすぎるとiOSのウォッチドックタイマによってアプリが強制終了させられます。
最後にメモリ使用量です。
メモリの使用量が多くても、通常はユーザーが気付くことはありません。
しかしメモリリークなどで大量のメモリを浪費すると iOS に強制終了させられます。
Instruments を使って、より完成度の高いアプリにしていきましょう。

今回はミュージックサーバーをコントロールするアプリを使って性能解析ツールの説明をします。
アルバムのサムネイルの一覧を表示し、選曲し、音楽再生中はアルバムのカバーを表示します。
ミュージックサーバーからアルバムのカバー画像を取得できなければデフォルトの画像を表示します。
下図の例では album003 というアルバムはカバー画像を取得できず、アプリのデフォルトのカバー画像を表示しています。

Debug Navigator

Instruments の説明の前に、Xcode のデバッガーで手軽に iPhone のリソースの消費量を確認できる Debug Navigator を紹介します。
Xcode のナビゲーションエリア(左パネル)の左から7つ目のボタン(下画面の赤丸)をクリックすると Debug Navigator が表示され、CPU、メモリ、エネルギー(バッテリー消費)の使用量、ストレージ、ネットワークへのアクセス量と速度がわかります。

下の画面はこのアプリのメモリ使用量を表示したものです。
アルバムのカバー画像取得のため一時的に60MBほどメモリを使いますが、処理が終われば解放されているのが確認できます。
このアプリは iPhone 全体のメモリのごく一部しか消費しておらず、メモリ使用量に関しては特に問題はありません。

なお次から説明する Instruments もそうですが、シミュレーターではなく iPhone の実機を使って性能解析をするようにしてください。
シミュレーターを使って解析しても Mac の性能を測ることになり、あまり意味がありません。
今回は比較的非力と思われる iPhone 6s を使用しました。
可能なら最新の機種と古い機種の2種類で確認すると良いでしょう。

Instruments

性能解析ツールのInstrumentsは、Xcodeのメニューで、

Xcode → Open Developer Tool → Instruments

で表示されます。
初回、およびソースコードを変更した場合は、必ず

Product → Build
Product → Profile

を実行してください。

Product メニューの Profile からも Instruments の画面が表示されます。
繰り返しになりますが、Xcode で実機が選択されていることを確認してください。
下図が Instruments のテンプレート選択画面です。

App Launch

ではユーザーにとっての最初のアプリ体験になる起動速度から確認していきましょう。
App Launch をクリックして選択し、Choose ボタンをクリックしてください。
App Launch の画面が表示されますので、左上の赤丸のボタン(①)をクリックしてください。
実機でアプリが起動され App Launch は5秒間データを収集した後、解析をして、下のような画面を表示します。

Time Profiler の列が CPU 使用率です。
SMP Controller の列の紫色の initializing と書いてある部分(②)は初期化中の段階でユーザーは操作できません。
いわゆる起動時間にあたります。
ここにマウスオーバーすると Initializing(4.19s) と表示されます。
起動に4秒以上かかっていることがわかります。
その右側の青い Foreground – Active と書いてある部分は初期化が終わって操作可能になった状態です。

では解析していきます。
SMP Controller の左の ▶︎(③)をクリックするとスレッドごと状態が表示されます。
Main Thread は UI をつかさどるスレッドで最も重要なのですが、処理はこの Main Thread に集中しています。
色の区別は以下の通りです。

青 Running:スレッドは処理中でCPU時間を使っています。
赤 Runnable:スレッドは実行可能な状態ですが、まだ処理を開始していません。
オレンジ Preempted:他の優先順位が高い処理により停止させられています。
灰色 Blocked:スレッドは I/O 待ちなどで休止中。例えば Main Thread がイベント待ちのアイドル状態など。

アプリの起動を速くするためには Main Thread の処理をオフロードする必要があります。
Main Thread がネットワークなどの I/O 待ちでブロックされる場合は、ババックグラウンド回して別のスレッドに処理させます。
起動直後に必要のない処理は、起動後にババックグラウンド処理させることもできます。

では Main Thread の処理を見てきます。画面下の Call Tree(①)をクリックして

Separate by Thread

をチェックし、Main Threadだけを追えるようにします。
次いで、

Hide System Libraries

をチェックします。
これでシステムライブラリが非表示になり自分のコードのシンボルだけが表示されますので原因究明がしやすくなります。
さらに

Invert Call Tree

をチェックするとシンボルの階層表示が逆順になり、CPU時間を使っているメソッドが特定しやすくなります。

次に Weight(②)をクリックして、CPU 時間を使っている順にソートします。
Main Thread が99%使っていますね。
49.5%は UIImage のイニシャライザが使っています。
これをコールしているのは getAlbums メソッドです。
クリックすると右側のパネルにスタックトレースが表示されます。
次いで getAlbum メソッドが16.7%使っています。

Symbol Name または Heaviest Stack Trace のシンボル名をダブルクリックするとソースコードの該当箇所が表示されますので、最も CPU 時間を使っている行を特定できます。
49.5%の CPU 時間を使っていたのは、アルバムのサムネイル画像をキャッシュから取得する処理でした。

let thumbnail = UIImage(contentsOfFile:thisAlbum.thumbnailURL!.path)

16.7%のCPU時間を使っていたのは、アーティストやジャンルなどアルバムの詳細データをミュージックサーバーから取得する処理でした。

if let album = getAlbum(mpdserver: mpdserver, name: name) {
albums += [album]
}

下の画面はシンボル名をダブルクリックして getAlbums() メソッドを表示した状態です。
メソッド名の左の ’Root’ をクリックすると元の画面に戻ります。
また赤丸(①)をクリックすると Xcode でソースコードの該当箇所が表示されますので、問題箇所を修正できます。

起動が遅い原因は、ミュージックサーバーに400枚以上のアルバムが入っているのに、起動時にそれらの詳細データをあらかじめすべて取得していたためでした。
そこで起動時にはこれらのデータを取得せず、アルバムのサムネイルが表示されるタイミングで、そのアルバムの詳細データとサムネイル画像を取得するようにプログラムを変更しました。
ソースコードを変更したので再び、

Product → Build
Product → Profile

を実行してから、App Launch を実行してみます。
Initializing の時間は677msになり、起動速度は飛躍的に速くなりました。
比較しやすいように上下に並べてみます(下画面)

Time Profiler

次は Instruments の Time Profiler で処理速度を解析します。
画面上部に Run 2 of 2 と表示されています。
その左の < をクリックしますと Run 1 of 2 となり前回実行の結果が表示されます。
実行のたびに記録を残して比較することができます。
2 of 2では、再生キューに1000曲ほど楽曲を入れて再生しました。
再生曲を変更して3回再生キューの再読み込みをしましたが、そのたびにかなりのCPU時間を使って、処理も遅いです。

App Launch の時と同じくWeightの大きいメソッドに注目します。
UIImage.resize() というメソッドが70.1%を占めていますね。
UIImage の resize というメソッドは、私が UIImage クラスを拡張して作ったメソッドです。
UIImage.resize() メソッドをリファクタリングする前に、それをコールしている extractSongData() メソッドを見てみましょう。
Symbol Name で Mpd.extractSongData() をダブルクリックすると、該当箇所のソースコードが表示されます。

われながら悪いコードです。
727行目でデフォルトのサムネイル画像を毎回作っています。
再生キューに1000曲あれば、1000回、同じサムネイル画像を作ります。
これはまったく不要な処理なので削除します。

同じく3回再生キューの再読み込みをしてCPU負荷を比較してみます。
上が修正前、下が修正後です。
かなり処理速度が改善されましたね。
平均的には1アルバムに10曲から20曲が入っていて、再生曲の切り替えは数分に1回でしょう。
1000曲を再生キューに入れて10秒ごと曲を変えて、この程度のCPU使用量と処理速度なら実用上問題ありません。

Leaks

最初に Debug Navigator でメモリの使用量を確認しましたので、このアプリはメモリに関してはあまり心配がないことがわかりました。
もしもメモリを大量に使う場合は、Instruments の Allocations でメモリ増加の要因になるオブジェクトとメソッドを特定できます。
最後に念のためにメモリリークを確認しておきます。

Leaksを実行すると定期的にメモリリークがあるかをチェックします。
赤いマークはリークがあったことを示します。
ググレーマークはこのタイミングでは新規のリークがなかったことを示します。
赤いマークをクリックするとリークの原因を表示します。
Stack Traceでシンボルをダブルクリックすると該当箇所のコードを表示します。

メモリリークを修正し、再び実行します。
緑のマークはメモリリークが発生していないことを示します。
メモリ負荷が高いカバー画像の取得をしてもメモリリークが発生しないこと、一時的に確保したメモリが解放されていること、を確認します。

なお、Leaks と Allocations は使用されているヒープ領域も確認できます。
Transient というのは、いずれ解放される一時メモリですので無視して結構です。
Persistent に注目してください。
上の画面では Persistent のヒープ領域は9.38MBなので問題ありません。

まとめ

Instruments を使えば、起動速度、処理速度、メモリ使用量などのボトルネックをピン・スポットで特定できます。
強力なツールですね。
Instruments のような性能解析ツールは一般的にはプロファイラと呼ばれ、Xcode だけでなく Microsoft Visual Studio をはじめ、殆ほとんど開発ツールがサポートしています。
ぜひ活用していきましょう。
4回にわたり iPhone アプリ開発のポイントを書籍ではなかなか得られない情報を中心に紹介してまいりましたが、いかがでしたでしょうか?
ぜひお仕事に活用ください。

お問い合わせ

Xcodeでの開発用にはMacレンタルをご利用ください。

追加で開発用機材が必要な時にご利用ください。

お気軽にお問い合わせください

ページの先頭に戻る