今日の共有内容は:基礎篇—— リリース段階で、主にクラッシュ、カクつき、メモリ、ログ、パフォーマンス、スレッド🧵、およびバッテリー🔋の監視についてです。
12 | iOS クラッシュの多様性、どのように包括的に監視するか?#
まずは一般的なクラッシュ原因を見てみましょう:
- 配列の問題:配列の境界を越えたり、nil 要素を配列に追加したりすること。
- マルチスレッドの問題:サブスレッドで UI 更新を行うとクラッシュが発生する可能性があります。例えば、あるスレッドがデータを null にしている間に、別のスレッドがそのデータを読み取っている場合です。
- メインスレッドの応答なし:メインスレッドの応答がシステムの規定時間を超えると、Watchdog によって強制終了されます。対応する異常コードは 0x8badf00d です。
- 野指針のアクセス:野指針が削除されたオブジェクトを指す場合、これは最も一般的でありながら最も特定が難しいクラッシュの状況です。
プログラムのクラッシュはユーザーにとって最も大きな損害をもたらすため、クラッシュ率(一定期間内のクラッシュ回数と起動回数の比率)は最も優先度の高い技術指標となります。
信号キャッチが可能かどうかに基づいて、クラッシュ情報は 2 つのカテゴリに分けられます:
- 信号キャッチが可能:配列の境界を越えた場合、野指針、KVO の問題、NSNotification スレッドの問題などのクラッシュ情報。
- 信号キャッチが不可能:バックグラウンドタスクのタイムアウト、メモリの過剰消費、メインスレッドのカクつきが閾値を超えた場合などの情報。
信号キャッチ可能なクラッシュログの収集#
簡単な方法:Xcode > Product > Archive を選択し、「Upload your app’s symbols to receive symbolicated reports from Apple」にチェックを入れます。これ以降、Xcode の Archive 内でシンボル化されたクラッシュログを見ることができます。
サードパーティのオープンソースライブラリ:PLCrashReporter、Fabric、Bugly。最初のものは自分のサーバーが必要ですが、後の 2 つはサーバー開発能力がないか、データに敏感でない企業に適しています。
- 監視の原理:さまざまな信号を登録し、異常信号をキャッチした後、処理メソッド handleSignalException 内で backtrace_symbols メソッドを使用して現在のスタック情報を取得し、スタック情報をローカルに保存します。次回起動時にクラッシュログをアップロードできます。
信号キャッチできないクラッシュ情報の収集#
背景知識:システムの制限により、システムによる強制終了が発生した信号はキャッチできません。
5 つの質問を持って:バックグラウンドでクラッシュしやすい理由は何ですか?バックグラウンドでのクラッシュを回避する方法は?バックグラウンドで信号キャッチできないクラッシュ情報を収集するにはどうすればよいですか?他に信号キャッチできないクラッシュの状況はありますか?他の信号キャッチできないクラッシュ情報をどのように監視しますか?
(一)バックグラウンドでクラッシュしやすい理由は何ですか?
まず、iOS バックグラウンドの生存方法を 5 つ紹介します:
- バックグラウンドモード:通常、地図、音楽、VoIP 系のアプリのみが審査を通過できます。
- バックグラウンドフェッチ:ウェイクアップ時間が不安定で、ユーザーはシステム設定でオフにできるため、使用シーンは少ないです。
- サイレントプッシュ:サイレントプッシュは、バックグラウンドでアプリを 30 秒間起動します。優先度は低く、application:didReceiveRemoteNotifiacation:fetchCompletionHandler: というデリゲートを呼び出します。これは通常のリモートプッシュ通知と同じデリゲートです。
- PushKit:バックグラウンドでアプリを 30 秒間起動し、主に VoIP アプリの体験を向上させるために使用されます。
- バックグラウンドタスク:アプリがバックグラウンドに退くと、デフォルトでこの方法が使用されるため、使用頻度が高いです。
バックグラウンドタスクのこの方法では、システムが beginBackgroundTaskWithExpirationHandler メソッドを提供してバックグラウンド実行時間を延長します。使用方法は以下の通りです:
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^(void) {
[self yourTask];
}];
}
このコードでは、yourTask タスクは最大 3 分間実行され、タスクが完了するとアプリがサスペンドされますが、タスクが 3 分以内に完了しない場合、システムはプロセスを強制終了し、これがアプリがバックグラウンドでクラッシュしやすい理由です。
(二)バックグラウンドでのクラッシュを回避するにはどうすればよいですか?
バックグラウンドでのデータの読み書き操作を厳密に制御します。例えば、処理するデータのサイズを確認し、データが大きすぎる場合、つまりバックグラウンド制限時間内に処理できない場合は、次回の起動時またはバックグラウンドでのアプリのウェイクアップ時に処理を検討できます。
(三)バックグラウンドで信号キャッチできないクラッシュ情報を収集するにはどうすればよいですか?
バックグラウンドタスク方式を使用する場合、まずタイマーを設定し、3 分に近づいたとき(beginBackgroundTaskWithExpirationHandler がバックグラウンドを 3 分間維持)にバックグラウンドプログラムがまだ実行中かどうかを判断します。まだ実行中であれば、そのプログラムがバックグラウンドでクラッシュしようとしていると判断し、すぐに報告・記録を行います。
(四)他に信号キャッチできないクラッシュの状況はありますか?
主にメモリが過剰消費される場合と、メインスレッドのカクつきがタイムアウトして Watchdog によって強制終了される場合の 2 つです。
(五)他の信号キャッチできないクラッシュ情報をどのように監視しますか?
バックグラウンドクラッシュの監視と似ており、閾値に近づいたときに処理を行います。詳細は次の 2 つのレッスンを参照してください~
収集したクラッシュ情報をどのように分析し、クラッシュ問題を解決しますか?#
クラッシュログには主に以下の情報が含まれています:
- 異常情報:異常の種類、異常コード、異常のスレッド;
- スレッドのトレース:クラッシュ時のメソッド呼び出しスタック。
- プロセス情報:クラッシュレポートの一意の識別子、一意のキー、デバイス識別子;
- 基本情報:クラッシュが発生した日付、iOS バージョン;
通常の分析プロセス:
- 「異常情報」にある異常スレッドを分析し、「スレッドのトレース」で異常スレッドのメソッド呼び出しスタックを分析します。シンボル化されたメソッド呼び出しスタックから、メソッド呼び出しのプロセスを完全に見ることができ、メソッド呼び出しスタックの頂点が最終的にクラッシュを引き起こしたメソッド呼び出しです。
- 異常コードを参考にします。ここでは44 種類の異常コードがリストされています。一般的な 3 つは:0x8badf00d(アプリが一定時間応答しなかったために watchdog によって強制終了された、詳細は次のレッスン)、0xdeadfa11(アプリがユーザーによって強制終了された)、0xc00010ff(アプリの実行によってデバイスの温度が高くなりすぎて強制終了された、詳細は 18 のバッテリー最適化のレッスン)。
⚠️:いくつかの問題はスタックだけでは分析できない場合があります。この場合、クラッシュ前のユーザーの関連行動やシステム環境状況のログを利用してさらに分析できます。詳細は 15 のログ監視のレッスンを参照してください。
考察:クラッシュ情報の収集効率を高め、喪失率を低くするにはどうすればよいか?より多くのクラッシュ情報を収集するにはどうすればよいか?特にシステムによる強制終了によって引き起こされるクラッシュについて。
13 | RunLoop の原理を利用してカクつきを監視するには?#
カクつきの問題とは、メインスレッドがユーザーのインタラクションに応答できない問題で、その原因には:UI 描画量が多すぎる、メインスレッドでネットワーク同期リクエストを行う、大量の IO 操作を行う、計算量が多すぎる、CPU が持続的に高占有、デッドロックとメイン・サブスレッドのロック競合などがあります。
NSRunLoopから始めると(スレッドのメッセージイベントは NSRunLoop に依存しています)、メインスレッドで呼び出されたメソッドを知ることができます。NSRunLoop の状態を監視することで、呼び出されたメソッドの実行時間が長すぎるかどうかを発見し、カクつきの状況を監視できます。
次に RunLoop の原理を紹介します~
RunLoop の原理#
目的:処理すべきイベントがあるときはスレッドを忙しく保ち、処理すべきイベントがないときはスレッドを休眠させること。
タスク:入力ソースを監視し、スケジュール処理を行う。
受け取る 2 種類の入力ソース(入力デバイス、ネットワーク、周期的な時間、遅延時間、非同期コールバック):
- 別のスレッドまたは異なるアプリからの非同期メッセージ;
- 予約された時間または繰り返し間隔の同期イベント。
アプリケーションの例:重い、緊急でない、大量の CPU を占有するタスク(例えば画像の読み込み)を空いている RunLoop モードで実行し、RunLoop モードが UITrackingRunLoopMode のときに実行を避けます。 UITrackingRunLoopModeモード:
- ユーザーがスクロール操作を行うと切り替わる RunLoop モード;
- このモードで重い CPU タスクを実行しないことで、ユーザーの操作体験を向上させることができます。
動作原理:
iOS では、RunLoop オブジェクトは CFRunLoop によって実装されており、全体のプロセスは以下の図に要約できます。具体的にはCFRunLoop のソースコードを参照してください。
loop の 6 つの状態#
コード定義は以下の通りです:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry , // loopに入る
kCFRunLoopBeforeTimers , // タイマーコールバックをトリガー
kCFRunLoopBeforeSources , // Source0コールバックをトリガー
kCFRunLoopBeforeWaiting , // mach_portメッセージを待つ
kCFRunLoopAfterWaiting , // mach_portメッセージを受信
kCFRunLoopExit , // loopを退出
kCFRunLoopAllActivities // loopのすべての状態が変わる
}
RunLoop のスレッドがブロックされる状況:
- 睡眠前のメソッドの実行時間が長すぎる(そのため睡眠に入れない);
- スレッドがウェイクアップした後、メッセージを受信する時間が長すぎる(次のステップに進めない)。
このスレッドがメインスレッドである場合、現れるのはカクつきです。
したがって、RunLoop の原理を利用してカクつきを監視するには、これらの 2 つの loop 状態に注目する必要があります:
- kCFRunLoopBeforeSources:睡眠に入る前に Source0 コールバックをトリガー;
- kCFRunLoopAfterWaiting:ウェイクアップ後に mach_port メッセージを受信。
カクつきをチェックするには?#
3 つのステップ:
- CFRunLoopObserverContext オブザーバーを作成;
- オブザーバーをメインスレッド RunLoop の共通モードで監視するために追加;
- メインスレッドの RunLoop 状態を監視するために持続的なサブスレッドを作成します。カクつきの判断:睡眠前の kCFRunLoopBeforeSources 状態、またはウェイクアップ後の kCFRunLoopAfterWaiting 状態が設定された時間閾値内で変化がない場合。
次に、スタック情報をダンプし、カクつきの原因をさらに分析できます。
⚠️:カクつきを引き起こす時間閾値は、WatchDog メカニズムに基づいて設定できます。
- 起動(Launch):20 秒;
- 復帰(Resume):10 秒;
- サスペンド(Suspend):10 秒;
- 退出(Quit):6 秒;
- バックグラウンド(Background):3 分(iOS 7 以前は、毎回 10 分を申請;その後、毎回 3 分を申請し、最大 10 分まで連続申請可能)。
PS:カクつきを引き起こす時間閾値は WatchDog の制限時間よりも小さくする必要があります。
カクつきのメソッドスタック情報を取得するには?#
1)システム関数を直接呼び出す(signal を使用してエラー情報を取得)。利点は性能消費が少ないこと;欠点は単純な情報しか取得できず、dSYM(シンボルテーブルファイル)と組み合わせて問題のあるコードを特定できないことです。性能が良いため、全体的なカクつき状況を観察するのに適していますが、カクつきの原因を特定する具体的なシーンには適していません。
2)PLCrashReporterオープンソースライブラリを直接使用します。特徴は、問題のコードの具体的な位置を特定でき、性能消費の最適化が行われています。
考察:なぜカクつき監視をオンラインで行うのか?主により広範囲に問題を収集するためです。カクつきの問題は、少数のユーザーデータの異常によって引き起こされることがよくあります。
関連資料:RunLoop の深い理解——ibireme
14 | OOM に近づいたとき、詳細なメモリ割り当て情報を取得し、メモリ問題を分析するには?#
OOM(Out of Memory):アプリがメモリを占有し、システムが単一のアプリに対して占有するメモリの上限に達した後、システムによって強制終了される現象。
- これは iOS の Jetsam メカニズム(オペレーティングシステムがメモリリソースの過度使用を制御するために採用したリソース管理メカニズム)によって引き起こされる「異常」なクラッシュです;
- ログは信号を通じてキャッチできません。
JetsamEvent ログを通じてメモリ制限値を計算する#
携帯電話の JetsamEvent で始まるシステムログを確認します(携帯電話の設定 > プライバシー > 分析と改善 > 分析データ)で、異なるマシンで異なるシステムバージョンにおけるアプリのメモリ制限を理解できます。
上記のシステムログでクラッシュ原因が per-process-limit に対応する rpages に注目します:
- per-process-limit:アプリが占有するメモリがシステムの単一アプリに対するメモリ制限を超えた;
- rpages:アプリが占有するメモリページの数。
⚠️:
- メモリページサイズの値は、ログ内の pageSize 値です。
- 強制終了されたアプリはシステムレベルのログを取得できず、オフラインデバイスを通じてのみ取得できます。
iOS システムが Jetsam を監視:
- システムは優先度の高いスレッド vm_pressure_monitor を起動し、システムのメモリ圧力状況を監視し、すべてのアプリのプロセスを維持するためのスタックを保持します。また、各プロセスのメモリページの消費状況を保存するためのメモリスナップショットテーブルも維持します。
- vm_pressure_monitor スレッドが特定のアプリのメモリに圧力を感じると、通知を発出し、メモリに圧力がかかっているアプリは対応する didReceiveMemoryWarning デリゲートを実行します(これはメモリを解放する機会であり、アプリがシステムによって強制終了されるのを回避できます)。
優先度判断基準(システムはアプリを強制終了する前に優先度を判断します):
- カーネル > オペレーティングシステム > アプリ;
- フロントアプリ > バックグラウンドで実行されるアプリ;
- スレッド使用の優先度では、CPU 占有が多いスレッドの優先度が低下します。
XNU を通じてメモリ制限値を取得する#
XNU のマクロを通じて memorystatus_priority_entry 構造体を取得することで、プロセスの優先度とメモリ制限値を得ることができます。
⚠️:XNU のマクロを通じてメモリ制限を取得するには root 権限が必要であり、アプリ内の権限は不十分なため、通常の状況下ではアプリ開発者はこの情報を見ることができません...
メモリ警告を通じてメモリ制限値を取得する#
didReceiveMemoryWarning というメモリ圧力デリゲートイベントを利用して動的にメモリ制限値を取得します。デリゲートイベント内で:
- まず、iOS システムが提供する task_info 関数を使用して、現在のタスクの情報(task_info_t 構造体)を取得します;
- 次に、task_info_t 構造体内の resident_size フィールドを通じて、現在のアプリが占有するメモリを取得します。
メモリ問題情報収集の特定#
メモリ占有量を取得するだけでは不十分で、誰がメモリを割り当てたのかを知る必要があります。そうすれば、問題の核心を正確に特定できます。すべての大きなメモリの割り当ては、外部関数がどのようにラッピングされていても、最終的にはmalloc_logger関数を呼び出します。
- メモリ割り当て関数 malloc や calloc などは、デフォルトで nano_zone を使用します;
- nano_zone は 256B 以下の小さなメモリの割り当てに使用され、256B を超えるメモリの割り当ては scalable_zone を使用します;
- scalable_zone でメモリを割り当てる関数はすべて malloc_logger 関数を呼び出し、システムはそれを通じてメモリの割り当て状況を統計し管理します。
したがって、fishhook を使用してこの関数をフックし、自分の統計記録を追加することで、メモリの割り当て状況を把握できます。
PS:メモリが大きすぎてシステムによって強制終了される以外にも、以下の 3 つのメモリ問題があります:
- 割り当てられていないメモリへのアクセス:XNU は EXC_BAD_ACCESS エラーを報告し、信号は SIGSEGV Signal #11 です。
- 権限を守らずにメモリにアクセス:メモリページの権限基準は UNIX ファイルの権限に似ています。読み取り専用権限のメモリページに書き込もうとするとエラーが発生し、XNU は SIGBUS Signal #7 信号を発出します。
- 割り当てられたが未提出のメモリへのアクセス:XNU は物理メモリの割り当てを阻止し、問題のあるスレッドがメモリページを割り当てるときにフリーズします。
最初の 2 つの問題はクラッシュログを通じて取得でき、12 のクラッシュのレッスンを参考にしてください。
15 | ログ監視:アプリ内の全ログを取得するには?#
背景:前回はクラッシュ、カクつき、メモリ問題の監視について共有しました。問題を監視したら、問題の詳細情報を記録し、ログを形成して開発者に通知する必要があります。そうすれば、開発者はこれらのログから問題を特定できます。
全ログの定義:アプリ内で記録されたすべてのログ、ユーザー行動や重要な操作のログを記録するために使用されます。
全ログの役割:開発者がさまざまな複雑な問題を迅速かつ正確に特定できるようにし、問題解決の効率を向上させます。
ただし、アプリは複数のチームによって共同で開発・維持される可能性が高く、異なるチームが使用するログライブラリは歴史的な理由から異なる場合があります。自分で開発したものか、サードパーティのログライブラリを使用しているかのいずれかです。では、どのように侵入しない方法でアプリ内のすべてのログを取得するのでしょうか?
以下にNSLogとCocoaLumberjackログの取得方法を紹介します。この 2 つのログ出力方法は、ほとんどのシーンをカバーしています。
NSLog のログを取得する#
NSLog は実際には C 関数void NSLog(NSString *format, ...);
であり、標準のエラーコンソールとシステムログに情報を出力する役割を果たします。
**NSLog のログを取得するには?** 方法は 3 つあります:
1)ASL が提供するインターフェースを使用します。
iOS 10 以前、NSLog は内部で ASL(Apple System Logger、Apple が独自に実装したログ出力システム)API を使用しており、ログメッセージを直接ディスクに保存していました。
サードパーティライブラリCocoaLumberjackの [DDASLLogCapture start] コマンドを利用して、すべての NSLog ログをキャッチし、CocoaLumberjack のログとして記録します。
キャッチの原理:
- ログが ASL データベースに保存されると、syslogd(システム内でログメッセージを受信・配信するためのログデーモン)が通知を発出します。
- notify_register_dispatch を通じてこの通知 com.apple.system.logger.message(kNotifyASLDBUpdate マクロ)を登録し、ログが ASL データベースに保存されたときに発出されるプロセス間のシステム通知です。
- 通知を受け取った後、ASL が提供するインターフェース(CocoaLumberjack は asl_search、asl_next、aslMessageReceived: などのメソッドをラップしています)を使用して新しいログを反復処理し、最終的に CocoaLumberjack のログとして記録します。
主なメソッドのコード実装は以下の通りです:
+ (void)captureAslLogs {
@autoreleasepool {
...
notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^(int token) {
@autoreleasepool {
...
// プロセス識別子を利用して、シミュレーターの場合は他のプロセスのログが無効になる通知を処理
[self configureAslQuery:query];
// すべての新しいログを反復処理(この通知で送られるログは複数ある可能性があります)
aslmsg msg;
aslresponse response = asl_search(NULL, query);
while ((msg = asl_next(response))) {
// ログを記録(CocoaLumberjackのログとして記録、デフォルトはVerboseレベル)
[self aslMessageReceived:msg];
lastSeenID = (unsigned long long)atoll(asl_get(msg, ASL_KEY_MSG_ID));
}
asl_release(response);
asl_free(query);
if (_cancel) {
notify_cancel(token);
return;
}
}
});
PS:
- CocoaLumberjack のログとして記録された後、さらに取得しやすくなります。詳細は次のセクションを参照してください。ログレベルには 2 つのカテゴリがあります:最初のカテゴリは Verbose と Debug で、デバッグレベルに属します。2 番目のカテゴリは Info、Warn、Error で、正式なレベルであり、永続的に保存する必要があり、より重要な情報の記録に適しています。ここではデフォルトで Verbose レベルです。
- NSLog デバッグを使用すると、IO ディスク操作が発生するため、頻繁に NSLog を使用することは性能に悪影響を及ぼします。
- システムディスクスペースが不足しているときなど、他にも多くのプロセス間通知があります。例えば、com.apple.system.lowdiskspace 通知(kNotifyVFSLowDiskSpace マクロ)が発出されます。
2)fishhook を使用して NSLog メソッドをフックします。
iOS 10 以降、ログを記録するために新しい統一ログシステム(Unified Logging System)が導入され、ASL の方法を全面的に置き換えました。
統一ログシステム:
- ログをメモリとデータベースに集中して保存し、システムのすべてのレベルのメッセージ伝達を取得するための単一、高効率、高性能のインターフェースを提供します;
- ただし、ASL のようにすべてのログを取得するためのインターフェースはありません。
したがって、新しい統一ログシステムに対応するために、NSLog ログの出力をリダイレクトする必要があります。また、NSLog 自体は C 関数であり、Objective-C メソッドではないため、fishhookを使用してリダイレクト作業を完了します:
struct rebinding
を使用して元のメソッドとリダイレクトメソッドを定義します。- リダイレクトメソッド内で:
- まず、自分の処理を行い、ログの出力を永続的なストレージシステムに再出力します;
- 次に、NSLog も呼び出す NSLogv メソッドを呼び出して元の NSLog メソッドを呼び出すことができます。また、fishhook が提供する元のメソッド呼び出し方式を使用できます。
3)dup2 関数を使用して STDERR ハンドルをリダイレクトします。
NSLog が最終的にファイルに書き込むときのハンドルは STDERR(standard error、システムのエラーログはすべて STDERR ハンドルを通じて記録されます)であり、Apple の NSLog の定義はエラー情報を記録することです。
dup2 関数はファイルリダイレクト専用で、STDERR ハンドルをリダイレクトすることができます。重要なコードは以下の通りです:
int fd = open(path_to_file, (O_RDWR | O_CREAT), 0644);
dup2(fd, STDERR_FILENO);
ここで、path_to_file
は自分で定義したリダイレクト出力のファイルパスです。
さて、各システムバージョンの NSLog ログはすでに取得できるようになりました。他の方法で記録されたログはどのように取得するのでしょうか?
次に、主流のサードパーティログライブラリ CocoaLumberjack ログの取得方法について説明します。他のサードパーティライブラリも CocoaLumberjack をラップしているため、考え方は似ています。
CocoaLumberjack ログの取得#
CocoaLumberjack は以下のように構成されています:
- DDLogFormatter:ログのフォーマットをフォーマットするためのもの。
- DDLogMessage:ログメッセージのラッピング。
- DDLog:グローバルなシングルトンで、DDLogger プロトコルに準拠するロガーを保存します。
- DDLogger プロトコル:DDAbstractLogger によって実装されており、4 つのロガーが DDAbstractLogger を継承しています:
- DDTTYLogger:ログをコンソールに出力します。
- DDASLLogger:NSLog を ASL データベースに記録するログをキャッチします。
- DDFileLogger:ログをファイルに保存します。
[fileLogger.logFileManager logsDirectory]
を使用して保存されたファイルパスを取得し、CocoaLumberjack のすべてのログを取得できます。 - DDAbstractDatabaseLogger:データベース操作の抽象インターフェース。
全ログを収集することで、分析と問題解決の効率を向上させることができます。さっそく試してみてください?
16 | パフォーマンス監視:アプリの品質を測る尺度#
目的:積極的かつ効率的にパフォーマンス問題を発見し、アプリの品質が無監視の失控状態に入るのを避けること。
監視方法:オフライン、オンライン。
オフラインパフォーマンス監視:公式の王牌 Instruments#
Instruments は Xcode に統合されており、以下のように、さまざまなパフォーマンス検出ツール(電力消費、メモリリーク、ネットワーク状況など)を含んでいます:
全体のアーキテクチャを見ると、Instruments は Standard UI(標準インターフェース)と Analysis Core(分析コア)の 2 つのコンポーネントを含んでおり、すべてのツールはこれらの 2 つのコンポーネントに基づいて開発されています。これらの 2 つのコンポーネントに基づいて、カスタム Instruments ツール(Instruments 10+)を開発することもできます:
- Xcode > File > New > Project > macOS > Instruments Package を選択し、.instrpkg ファイルを生成します;
- このファイルを設定し、最も重要なのは Standard UI と Analysis Core の設定を完了することです;
- Apple が提供する多数のコードスニペットを参考にしてください。詳細はInstruments Developer Helpを参照してください。
Analysis Core の動作原理:
主にデータを収集し処理するプロセスで、3 つのステップに分かれています:
1)設定した XML データテーブル(可視化表示用)を処理し、ストレージスペース store を申請します。
2)store は相応のデータプロバイダーを探し、直接見つからない場合は他の store の入力信号を通じて合成します。
⚠️:os_signpost APIを使用してデータを取得します。詳細はWWDC 2018 Session 410: Creating Custom Instrumentsの例を参照してください。
3)store がデータソースを取得した後、データ処理プロセスを最適化するために Binding Solution 作業を行います。
PS:Instruments は XML 標準データインターフェースを通じて表示と分析データの思考を分離しているため、学ぶ価値があります。
オンラインパフォーマンス監視#
2 つの原則:
- ビジネスコードに侵入しない;
- パフォーマンス消費をできるだけ小さくする。
主な指標:
CPU 使用率#
現在のアプリ CPU の使用率、つまりアプリ内の各スレッドの CPU 使用率の合計です。したがって、定期的に各スレッドを走査し、各スレッドの cpu_usage 値を累積すればよいのです。
⚠️:
task_threads(mach_task_self(), &threads, &threadCount)
メソッドを使用して、現在のプロセス内のすべてのスレッドの配列 threads とスレッドの総数 threadCount を取得できます。thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount)
メソッドを使用して、スレッド threads [i] の基本情報 threadInfo を取得できます。cpu_usage
は iOS システム > usr/include/mach/thread_info.h > thread_basic_info 構造体で定義されています。
メモリ#
CPU 使用率と同様に、メモリ情報も専用の構造体で記録されており、iOS システム > usr/include/mach/task.info.h > task_vm_info 構造体で定義されています。その中の phys_footprint が物理メモリの使用を示します。
⚠️:
task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &vmInfoCount
メソッドを使用して、現在のプロセスのメモリ情報 vmInfo を取得できます。- 物理メモリは
phys_footprint
で表され、resident_size(常駐メモリ:プロセスの仮想メモリ空間にマッピングされた物理メモリ)ではありません。
FPS#
FPS が低いと、アプリがスムーズでないことを示します。
簡単な実装:CADisplayLink に登録されたメソッド内で、リフレッシュ時間とリフレッシュ回数を記録することで、1 秒間に画面がリフレッシュされる回数、つまり FPS を得ることができます。
⚠️:画面がリフレッシュされるたびに CADisplayLink に登録されたメソッドが 1 回呼び出されます。
ヒント:
- サードパーティの監視プラットフォームの推奨:アリババモバイル開発プラットフォーム mPaaS。
- Apple 自身のライブラリやツールに多くの注目を払い、ここにあるデザイン思想や進化から学ぶべき知識が豊富にあります。
17 | 想像以上のマルチスレッドの落とし穴#
現象:AFNetworking 2.0(ネットワークフレームワーク)、FMDB(サードパーティデータベースフレームワーク)などの一般的な基盤ライブラリは、マルチスレッド技術を使用する際に非常に慎重です。特に UIKit はマルチスレッド技術を使用せず、スレッドセーフではなく、主スレッドでのみ操作できます。
なぜこのような現象があるのでしょうか?以下にマルチスレッド技術の一般的な 2 つの大きな落とし穴を見てみましょう:常駐スレッドと並行問題。
常駐スレッド#
定義:停止せず、メモリ内に常に存在するスレッド。
どこから来たのか?
NSRunLoop のrun
メソッドを使用してそのスレッドに runloop を追加すると、そのスレッドはメモリ内に常に存在します。
例:AFNetworking 2.0で常駐スレッドを作成するコードは以下の通りです:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
// まずNSThreadを使用してスレッドを作成します
[[NSThread currentThread] setName:@"AFNetworking"];
// runメソッドを使用してrunloopを追加します
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
AFNetworking 2.0 は各リクエストを NSOperationQueue にラップし、上記の常駐スレッドを特別に作成して NSOperationQueue のコールバックを受け取ります。
常駐スレッドの落とし穴を避けられなかった理由は、ネットワークリクエストに使用されるNSURLConnection に設計上の欠陥があるためです:NSURLConnection がリクエストを発起した後、そのスレッドは NSURLConnectionDelegate のコールバックメソッドを受け取るために常に生存している必要があります。しかし、ネットワークの応答時間は不確定であるため、常駐スレッドが必要です。そして、主スレッドを使用せずに独自のスレッドを作成するのは、主スレッドが大量の UI やインタラクション作業を処理する必要があるからです。
🎉:ただし、AFNetworking 3.0 では、Apple が新たに導入した NSURLSessionが NSURLConnection に置き換えられ、NSURLSession はコールバックを NSOperationQueue として指定できるため、リクエストのコールバックを待つために常駐スレッドを必要としなくなりました。
どうやって避けるか?
常駐スレッドが多すぎると、CPU の利用率を向上させることができず、逆にプログラムの実行効率を低下させます。
常駐スレッドを作成しないのが最善ですが、もしスレッドを一定期間生存させる必要がある場合は、次の方法を選択できます:
- NSRunLoop の他の 2 つのメソッド
runUntilDate:
とrunMode:beforeDate:
を使用してスレッドの生存時間を指定し、スレッドの生存時間を予測可能にします。 - CFRunLoopRef の CFRunLoopRun と CFRunLoopStop メソッドを使用して runloop を開始および停止し、スレッドを一定期間生存させる目的を達成します。
⚠️:NSRunLoop に runloop を追加する方法にはrun
、runUntilDate:
、runMode:beforeDate:
の 3 つがあります。その中で、run
メソッドで追加された runloop は、runMode:beforeDate:
メソッドを繰り返し呼び出して停止しないようにします。
並行#
どこから来たのか?同時に複数のスレッドを作成しました。
iOS の並行プログラミング技術では、GCD(Grand Central Dispatch)の使用率が最も高く、Apple が開発したマルチコアプログラミングソリューションです。
- 利点:インターフェースがシンプルで使いやすく、複雑なスレッド(作成、解放のタイミングなど)を管理しやすい。
- 欠点:リソース使用にリスクが存在します。例えば、データベースの読み書きシーンでは:
- 読み書き操作がディスクの応答を待っているときに、GCD がタスクを発起します;
- CPU を最大限に活用する原則に基づいて、GCD はディスクの応答を待っているこの隙間で新しいスレッドを作成します。
- もし GCD が発起したこれらの新しいタスクがすべてディスクの応答を待つタスクである場合、タスクの数が増えるにつれて、GCD が作成する新しいスレッドがますます増加し、メモリリソースがますます逼迫します。
- ディスクが応答を開始すると、データを再読み込みするのにさらに多くのメモリを占有し、最終的にはメモリ管理が失控します。
どうやって避けるか?
データベースのように頻繁にディスク操作を読み書きするタスクでは、できるだけシリアルキューを使用して管理し、マルチスレッドの並行によるメモリ問題を避けるべきです。
推奨:オープンソースのサードパーティデータベースフレームワークFMDBは、そのコアクラス FMDatabaseQueue がデータベースに関連するディスク操作をすべてシリアルキューに配置して実行します。
⚠️:スレッドが多すぎると、メモリとCPUの消費が大きくなります。
- システムはスレッドスタックとして一定のメモリを割り当てる必要があります。iOS 開発では、メインスレッドのスタックサイズは 1MB で、新しく作成されたサブスレッドのスタックサイズは 512KB(スタックサイズは 4KB の倍数)です。
- CPU はスレッドのコンテキストを切り替えるときに、アドレス指定を通じてレジスタを更新する必要があります。アドレス指定のプロセスにはかなりの CPU 消費が伴います。
ヒント:マルチスレッド技術におけるロックの問題は最も簡単に見つかるものであり、逆に注意すべきは、背後に隠れていてシステムリソースを徐々に消耗する問題です。
18 | アプリの電力消費を減らすには?#
過剰な電力消費の可能性のある原因:位置情報をオンにしている;頻繁なネットワークリクエスト;定期タスクの時間間隔が小さすぎる...
具体的な位置を特定するために排除法を使用します:機能を 1 つずつコメントアウトし、電力消費の変化を観察します。
しかし、言うまでもなく、まず電力を取得しなければ、電力問題を発見することはできません。
電力を取得するには?#
システムが提供する batteryLevel プロパティを使用します。コードは以下の通りです:
- (float)getBatteryLevel {
// 電力を監視するには、必ず許可を与える必要があります
[[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];
// 0.0(電池切れ)、1.0(満充電)、–1.0(バッテリー監視がオフ)
float batteryLevel = [[UIDevice currentDevice] batteryLevel];
NSLog(@"残りのバッテリー比率:%@", [NSString stringWithFormat:@"%f", batteryLevel * 100]);
return batteryLevel;
}
参考:batteryLevel —— Apple 公式文書。
PS:電力変化通知にオブザーバーを追加し、電力が変化したときにカスタムメソッドを呼び出すことで電力を監視できます。参考:UIDeviceBatteryLevelDidChangeNotification —— Apple 公式文書。
電力問題を診断するには?#
上記の排除法を使用しても問題が見つからない場合、この電力消費は他のスレッドによって引き起こされている可能性が高く、この電力消費スレッドが作成される場所はサードパーティライブラリまたは二方ライブラリ(社内の他のチームが開発したライブラリ)である可能性があります。
このような状況に直面した場合、どのスレッドが問題であるかを直接観察します。たとえば、特定のスレッドのCPU 使用率が長時間 90%を超えている場合、それが問題であると推測できます。この時、メソッドスタックを記録して根本原因を追跡できます。
- CPU 使用率を観察するには、16 のレッスン | オンラインパフォーマンス監視の部分を参照してください。
- メソッドスタックを記録するには、13 のレッスン | カクつきのメソッドスタック情報を取得する部分を参照してください。
電力を最適化するには#
CPU の観点から#
CPU に余計なことをさせないようにします。
- 大量のデータの複雑な計算は、サーバーに処理させます。
- アプリ内で処理する必要がある複雑な計算は、GCD の
dispatch_block_create_with_qos_class
メソッドを使用してキューの Qos を QOS_CLASS_UTILITY に指定し、計算作業をこのキューのブロックに配置します。なぜなら、この Qos モードでは、大量のデータの複雑な計算に対してシステムが電力最適化を行っているからです。
I/O の観点から#
あらゆる I/O 操作は、低消費電力状態を破壊します。
- フラグメント化されたデータのディスクストレージ操作を遅らせ、まずメモリ内で集約し、その後ディスクストレージを行います。
- システムが提供する NSCache を使用して、メモリ内でデータを集約できます:
- スレッドセーフです。
- 事前に設定されたキャッシュスペース値に達するとキャッシュをクリアし、
cache:willEvictObject:
コールバックメソッドをトリガーし、このコールバック内でデータに対して I/O 操作を行います。
関連ケース:SDWebImage画像読み込みフレームワークがキャッシュ画像を読み取ります。
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
// NSCacheに画像データが存在するか確認します
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
// もしあれば
return image;
}
// もしなければ、ディスクから読み込みます
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// そしてNSCacheに保存します
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
- 画像を読み込むたびに、NSCache に画像データがすでに存在するかどうかを確認します。
- もしあれば、NSCache から直接読み込みます;
- もしなければ、I/O を通じてディスクキャッシュ画像を読み込み、取得した画像データを NSCache に保存します。
Apple の参考#
- “Energy Efficiency Guide for iOS Apps”:Apple が特別に維持している電力最適化ガイドで、CPU、デバイスのウェイクアップ、ネットワーク、グラフィックス、アニメーション、ビデオ、位置情報、加速度計、ジャイロスコープ、磁力計、Bluetooth などの多くの要因から電力最適化に関する提案がなされています。
- “Writing Energy Efficient Apps”:Apple が 2017 年の WWDC でのセッション 238 で共有した、エネルギー効率の良いアプリを書く方法に関するテーマです。
19 | ホットな問題の Q&A(二):基礎モジュールの問題 Q&A#
RunLoop 原理の学習順序#
- 孫源のオフラインシェア | RunLoop:RunLoop の全体像を大まかに理解します。
- RunLoop 公式文書:Apple が設計した RunLoop メカニズムを全面的かつ詳細に理解し、RunLoop を使用して問題を解決する方法を学びます。
- ibireme | RunLoop の深い理解:底層 CFRunLoop のソースコードを結びつけて、RunLoop メカニズムを深く分析します。
dlopen () を使用すると審査に通過できますか?#
dlopen()
を使用してリモートダイナミックライブラリを読み込むことは、Apple の審査に通過できません。
Apple は 2018 年 11 月に 718 のアプリを集中してオフラインにした際に、dlopen()
、dlsym()
、respondsToSelector:
、performSelector:
、method_exchangeImplementations()
などのメソッドを使用してリモートスクリプトを実行することは許可されていないと述べました。なぜなら:
- これらのメソッドはリモートリソースと結びついており、プライベートフレームワークやプライベートメソッドを読み込む可能性があり、アプリの動作に重大な変化をもたらすため、審査時の状況とは異なることになります。
- 使用されるリモートリソース自体が悪意のあるものでなくても、それらは簡単にハイジャックされ、アプリケーションにセキュリティの脆弱性をもたらし、ユーザーに予測できない損害を与える可能性があります。
matrix-iOS#
WeChat がオープンソースしたカクつき監視システムです。
matrix-iOS はアプリのパフォーマンス損失を最小限に抑えるための 4 つの詳細があります:
- サブスレッドの監視検出時間間隔:カクつきを監視するサブスレッドは NSThread を通じて作成され、正常な状況では検出時間間隔は 1 秒ですが、カクつきが発生した場合、間隔時間はアニーリングアルゴリズムの影響を受け、フィボナッチ数列に従って増加し、カクつきがなくなると 1 秒に戻ります。
- サブスレッド監視のアニーリングアルゴリズム:同じカクつきを繰り返し取得することを避けます。
- RunLoop カクつき時間閾値設定:2 秒。
- CPU 使用率閾値設定:単一コア CPU の使用率が 80%を超えると、CPU 占有が高すぎると判断します。
参考:
- matrix for iOS/macOS/Android、主なコードはmatrix/WCBlockMonitorMgr.mmファイルにあります;
- matrix-iOS カクつき監視原理;
- 初期の WeChat iOS カクつき監視システムの提案。
教えることによって学び、復習することで新しい知識を得る~