モツモツ とは、モバイルアプリが遅く不定期に動作する状況をユーザーが記述するものです。つまり、通常は正常に反応しますが、突然数秒間フリーズすることがあります。技術的な文脈では、“モツモツ”は頻繁なGCポーズ、同期操作によるメインスレッドのブロック、非最適なデータ構造によって引き起こされるラグとマイクロフリーズの組み合わせを意味します。Android Performance Benchmarking Guideによると、応答時間を300msから100msに減らすと、ユーザーの継続利用率が25%向上します。モツモツの診断には、ガベージコレクションの頻度解析と並んでCPUおよびメモリのプロファイリングが必要です。
まとめ
モツモツは、ユーザーが主観的に遅いアプリケーションを記述するために使用する非正式な用語です。一定の遅延として現れるラグとは異なり、モツモツは不規則なフリーズから構成されます:アプリは数秒間完璧に動作したあと、突然1–3秒間“考えて”いるようになることがあります。
プロファイリングの観点からは、モツモツは、ピーク遅延が100msを超える一連のフレームドロップ(jank)として現れます。FPSグラフでは、これは急激なドロップとして見えます:60→20→55→10フレーム/秒。一定的にFPSが低いラグとは異なり、モツモツには時間変動があります。
アプリがモツモツすると、ユーザーには遅くなった理由がわかりません。画面が滑らかにスクロールしていたかと思うと、突然一秒間停止することがあります。これは節になるもので、アプリへの信頼を損ないます。Googleによると、ローディングに3秒以上かかる場合、53%のユーザーがサイトまたはアプリを離れます。
モツモツの閃発的性質は、問題が一定的な過負荷ではなく、イベント驅動因子によって引き起こされていることを示しています。具体的なシナリオを見てみましょう。
AndroidのARTランタイムでは、ガベージコレクションはすべてのアプリスレッドを停止します。コードが多くの一時オブジェクトを作成する場合、例えば毎回のonBindViewHolderコールで結合によって新しいStringを作成する場合、GCはより頻繁に実行されます。ポーズは、ヒープサイズとオブジェクトの生成世代によって、5–50msの間続することがあります。ユーザーはこれを突然の“考え深げ”として感知します。
AndroidのRoomやiOSのCore Dataは非同期クエリをサポートしていますが、開発者はよく簡単さのためにgetValue()を呼んだり、runBlockingを介してクエリを実行したりします。10,000行のテーブルに対するJOINを伴う重いSELECTは200–500msかかり、その間UIを完全にブロックします。
カメラ画像(12MP、4000x3000px)を縮小せずにロードすると、Bitmapへのデコードに200msまでかかります。画像を非同期的にロードしていても、制限のないスレッドプールを使用している場合、5–6のデコードを同時実行するとCPUに過負荷がかかり、移動的な動きの遅さが生じる可能性があります。
閃発的な動作遅延の診断は、一定的なラグの診断よりも難しいです。なぜなら、問題が毎回の実行で再現するとは限らないからです。長期的な統計データの収集が必要です。
Android Studio Memory Profilerは、メモリ使用量だけでなく、GCイベント(頻度、タイプ(Concurrent、Full)、期間)も表示します。アイドル状態で5秒に1回以上GCが発生する場合から、これは過剰な割り当てのサインです。モツモツの瞬間にヒープダンプを取ると、どのオブジェクトがメモリを占用しているかがわかります。
iOSでは、InstrumentsのAllocationsテンプレートを使用して、オブジェクトの作成と解放をトレースします。Generationsを有効にすると、アクションの間にヒープのスナップショットを取り、メモリに残っているオブジェクトを確認できます。解放されない永続オブジェクトは、メモリの蓄積とその後のポーズの原因となります。
JankStatsは、リアルタイムでフレームドロップメトリクスを収集するAndroidライブラリです。各jankを現在のシナリオ(例えば「リストスクロール」「画面開封」)に結びつけることで、どのアクションがモツモツを引き起こすのかを特定できます。
AndroidでJankStatsを組み込んでフリーズをトレースする例:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
モツモツを解消するには、各原因に対して目的を持った取り組みが必要です。万能の解決策はありません。診断には、個々のパフォーマンスプロファイルの解析が必要です。
リストに1000+の項目があり、すべてを一度にロードする場合、それは確実にモツモツを引き起こします。AndroidのPaging 3やiOSのNSFetchedResultsControllerは、ユーザーがスクロールするにつれデータを分割って読み込みます。ユーザーには最初の10–20項目のみが表示され、残りはバックグラウンドで読み込まれます。
Roomは、Android StudioのInspection Toolを介してクエリのプロファイリングを可能にします:実行時間、戻された行数、クエリプランが表示されます。WHEREやORDER BY列にインデックスを追加すると、クエリ時間を300msから5msに減らせます。iOSでは、InstrumentsのCore Data Profilerが同様の検査を行います。
バックグラウンド同期、ファイルダウンロード、データ処理—これらはすべてWorkManager(Android)またはBackground Tasks(iOS)を介して実行すべきです。同期がUIスレッドで実行される場合、実行中にアプリがモツモツします。WorkManagerは、バッテリーおよびネットワークの状態を考慮した上で、バックグラウンドスレッドでの実行を保証します。
AndroidでWorkManagerを介したバックグラウンド同期の例:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("シンク", "バックグラウンドスレッドでデータをシンク中")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
効率的なメモリおよびスレッド管理の原則に従うことで、コーディング階階でモツモツを予防できます。
Baseline Profilesは、AndroidがJITではなく事前にコンパイル(AOT)するクラスとメソッドのリストです。プロファイルがない場合、新しい画面は初回開いた時にコンパイルされ、100–500msの遅延が生じます。主な画面のBaseline Profileを作成し、baseline-profile-gradle-pluginを介してGradleで生成を有効にします。
ホットパスは、毎フレームで実行されるコードです:onBindViewHolder、draw、layoutSubviews。これらのメソッドでオブジェクトを作成するのを避けます:オブジェクトプールを使用し、結合の代わりにStringBuilderを使用し、フォーマットされた文字列とフォーマッターをキャッシュします。余計な割り当ては次のGCを近づけます。
CIパイプラインに、リストスクロールと画面開封のシナリオを含むMacrobenchmarkを追加します。スレッショルドを設定します:フレーム時間の99パーセンタイルは16msを超えないものとします。この値を超えた場合、ビルドは最適化が完了するまで拒否されます。
よくある質問
ラグは一定的な遅延です(例えば、毎回のタップで200ms)。モツモツは閃発的です:アプリは正常に動作し、突然1–3秒間遅くなり、また正常に戻ります。原因は、GCポーズやデータベースへの同期クエリなどのイベント驅動因子です。
Android StudioのMemory Profilerを使用します:Memoryタブは、期間付きのGCイベントを表示します。プロダクション監視には、カスタムトレース付きのFirebase Performance Monitoringを組み込みます。iOSでは、Malloc Debugを有効にし、Instrumentsで割り当てのジェネレーションをマークします。
間接的には—はい。サーバーからの応答が遅れ、UIが同期的に待っている場合、アプリはフリーズします。リクエストが非同期であっても、応答処理がUIスレッドで行われる場合もモツモツが生じます。解決策は、コルーチンと進捗インジケーターを使用した非同期処理です。
正しく使用されない場合、KMPは相互運用性のために過剰なラッパーオブジェクトを生成する可能性があります。iOSでは、これにより割り当ての頻度が増え、その結果ARCポーズが増えます。@ObjCNameを使用し、expect/actualを最適化し、UIのホットパスからの共有コードへの頻繁なコールを避けます。
android:largeHeap=”true”によるヒープの拡張は、GCを遅らせますが、割り当ての原因を解消しません。GCが実際に実行されるとき、より多くのオブジェクトを巡る必要があるため、ポーズはより長くなります。解決策は、ヒープを拡張することではなく、割り当ての数を減らすことです。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。