Upgrade to Pro — share decks privately, control downloads, hide ads and more …

【DroidKaigi2026】あなたのANRはどこから?

Avatar for NRI Netcom NRI Netcom PRO
September 17, 2026

 【DroidKaigi2026】あなたのANRはどこから?

Avatar for NRI Netcom

NRI Netcom PRO

September 17, 2026

More Decks by NRI Netcom

Other Decks in Technology

Transcript

  1. 序章 ANR ◼Application Not Responding ◼アプリが反応しなくなること Android アプリの UI スレッドが長時間ブロックされると、アプリケーション応答なし(ANR)

    エラーが発生します。 アプリがフォアグラウンドにある場合、(中略)ユーザーにダイアログが表示されます。ANR ダ イアログにより、ユーザーはアプリを強制終了できます。 Android Developers — ANR Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 2
  2. 序章 ANRは Android 独自 ◼iOS:正式名なし ⚫ watchdog termination ⚫ ダイアログなし

    ◼Web:ページ無応答 ⚫ Chrome の「このページは応答しません(待機/終了)」 ◼ダイアログ出して待つ/閉じるをユーザーに選択させるANRは Android 独自のもの Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 3
  3. 序章 Google Play が数値で見張っている ◼ANR率がしきい値を超えると ⚫ 見つかりにくくなる:おすすめなどの掲載面から外される ⚫ 競合アプリに誘導される:その端末では「別のアプリを」と促される アプリが不正な動作のしきい値を超えると、Google

    Play で見つかりにくくなる可能性があります。アプリの不正な動作が特定の デバイスモデルで発生する場合、Google Play は、そのデバイスでは該当するタイトルの使用をやめて、より適した別のアプリを使 用するようユーザーを誘導します。 Android Vitals — Core technical quality(不正な動作のしきい値) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 5
  4. 序章 Google Play が数値で見張っている ◼端末単位でしきい値を超えると、ストアの掲載情報に警告が 表示される ⚫ インストールボタンの真下に「この端末では正常に動作しない可能性」 ⚫ その下に別アプリを探す導線まで出る

    場合によっては、ユーザーが事前に認識できるようアプリのストアの掲載情報 に警告が表示され、技術的品質の高い別のアプリを探すためのオプションが 提供されることもあります。 Android Vitals — Core technical quality(不正な動作のしきい値) 出典: Android Developers Blog 「Raising the bar on technical quality on Google Play」(2022) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 6
  5. 序章 しきい値は思ったより厳しい 全体的な不正な動作: すべてのデバイスモデルで、1 日のアクティブ ユーザーの 0.47% 以上が ANR を認識しています。

    デバイスごとの不正な動作: 1 つのデバイスモデルで、1 日のアクティブ ユーザーの 8% 以上が ANR を認識しています。 Android Vitals — ユーザーが認識した ANR 発生率のしきい値 ◼たった 0.47%(全端末)/単一機種 8% を超えると「不良」判定 ◼参考: クラッシュ率は 1.09% = ANRのほうがしきい値は厳しい Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 7
  6. 序章 ANRは目をそらしたくなる ◼何が原因でアプリが固まったのか見当もつかない ⚫ コードで処理を追えても、手元では再現しない ◼Crashlytics や Vitals を見ても、書いてあることが今ひとつピンとこない ⚫

    スタックトレースが長すぎて、どこを見ればいいのか分からない ⚫ 分析情報のワードがピンとこない(「バインダー呼び出し」って?) ◼ANRが出ていてもアプリは動く Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 8
  7. 序章 自己紹介 ◼西村 蝶海(にしむら ちょうみ) ◼NRIネットコム株式会社 ⚫ 2020年 新卒入社・クラウド事業推進部 ◼モバイルアプリ開発チーム

    ⚫ 2021年から toC 向けアプリ開発 ⚫ Android チーム(6名)のリーダー 趣味 ◼初 CfP&初登壇 ◼山口県出身 クラシックバレエ ピクミンブルーム 編み物 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 9
  8. 序章 このセッションで共有したいこと ◼症状 — 3種類のANR、その中で何が起きているのか ◼診断 — スタックトレース等のどこを見るか ◼処方 —

    症状別に、どう直しどう防ぐか ANRを見る勘所は、そのままパフォーマンスの良いアプリを作る勘所になります Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 10
  9. 序 ANRとは? 序 章 何がまずいのか いまここ 1 2 3 終

    メインスレッドI/O 第 1 章 Repository の I/O が main を止めるまで Binder 呼び出しの遅延 第 2 章 別プロセスの返事を main で待つ ロック競合 第 3 章 「held by」を辿って犯人を見つける 持ち帰りの診断チャート 終 章 症状 → 型 → 処方 を1本の図で Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 11
  10. 第1章 | きっかけ 分析情報を見てもピンとこない ◼Crashlytics の表示は「メインスレッドでの I/O」(Vitals でも同じ分類) ◼そう言われてもピンとこない ⚫

    普段はKMPで、担当は View〜ViewModel 層だけ ⚫ 実装の中でスレッド管理を意識することがなかった ◼自分の担当範囲を超えて、Repository 層まで深掘りすることにした Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 14
  11. 第1章 | 基礎 メインスレッド(UIスレッド) ◼アプリ起動時に 1本だけ 作られる。描画・タップ・ライフサイクルを全部この1本で捌く 「Android のシングル スレッド

    モデルには次の 2 つのルールがあります。 1. UI スレッドをブロックしないでください。 2. UI スレッドの外部から Android UI ツールキットにアクセスしないでください。」 — Android Developers — プロセスとスレッド Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 15
  12. 第1章 | 基礎 ワーカースレッド 重たい処理はワーカースレッドに逃がす ◼メインスレッドのルール1「ブロックしない」重い仕事はメインの外に出す ⚫ メインスレッド以外のスレッド = ワーカースレッド

    ◼コルーチンでは、どのスレッドで動かすかを「ディスパッチャ」が決める ⚫ Dispatchers.IO:待つ仕事用。待つ間スレッドは暇なので、多めに持てる(既定 最大64) ⚫ Dispatchers.Default:計算する仕事用 CPUコア数に応じた並列速度で動くため、スレッドを増やしても大幅には早くならない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 17
  13. 第1章 | ③5秒でANR 捌けないまま5秒 → ANR Input dispatching timed out

    ... Waited 5000ms for MotionEvent Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 20
  14. 第1章 | トレースの読み方 スタックトレースを読む main (native):tid=1 systid=24983 #00 libc.so (syscall

    + 28) #01 libart.so (art::ConditionVariable::WaitHoldingLocks + 136) #05 SQLiteConnection.executeForLastInsertedRowId #07 SQLiteStatement.executeInsert #09 SQLiteDatabase.insert #14 RawSqliteStatsHelper.insertBlocking #27 BlockingStatsRepository.dailyStats #31 StatsViewModel$1.invokeSuspend ◼メインスレッド上で SQLite の insert ◼Crashlytics や Vitals はこのスタックを機械的に読んで解析情報を出している Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 21
  15. 第1章 | トレースの読み方 下に遡って修正すべき箇所を特定する main (native):tid=1 systid=24983 #00 libc.so (syscall

    + 28) #01 libart.so (art::ConditionVariable::WaitHoldingLocks + 136) #05 SQLiteConnection.executeForLastInsertedRowId #07 SQLiteStatement.executeInsert #09 SQLiteDatabase.insert #14 RawSqliteStatsHelper.insertBlocking #27 BlockingStatsRepository.dailyStats #31 StatsViewModel$1.invokeSuspend ◼遡ると ViewModel→Repository→Helper の処理の流れが見える ※ トレースはダンプが撮られた一瞬。詰まりが解けた後だと main は nativePollOnce(=暇)しか写らないこともある Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 22
  16. 第1章 | 原因 Room でのDB操作の実装 // DAO:クエリを suspend 関数として定義する @Dao

    interface UserDao { @Query("SELECT * FROM user WHERE id = :id") suspend fun loadUserById(id: Int): User } // 呼び出し側:Main のまま呼ぶだけ viewModelScope.launch { val user = dao.loadUserById(id) } 出典: Android Developers「非同期 DAO クエリを作成する」 ◼呼ぶと Room が内部のクエリ実行スレッドで実行してくれる ◼呼び出し側はスレッドを一切書いていない = 詰まらない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 23
  17. 第1章 | 原因 生の SQLite でのDB操作の実装 // 公式の読み出し例(Android 標準API) val

    db = dbHelper.readableDatabase val cursor = db.query(FeedEntry.TABLE_NAME, projection, selection, ...) 出典: Android Developers「SQLite を使用してデータを保存する」 ◼query() は普通の同期メソッド。逃がす人がいないので、呼んだスレッドでそのまま走る ◼メインスレッドでそのまま呼んだ場合はANRのリスクを埋め込むことになる ◼同じ SQLite でも、スレッド切り替えまで面倒を見てくれるとは限らない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 24
  18. 第1章 | 原因 メインスレッドI/OのANRはどこから? ◼使っていたのは 生に近いSQLite(SQLDelight) ◼スレッド切り替えをしていなかった = DB操作がそのままメインスレッドで走っていた 長時間にわたって実行される可能性があるため、バックグラウンド

    スレッドで getWritableDatabase() または getReadableDatabase() を呼び出してください。 — Android Developers — SQLite を使用してデータを保存する 注意書きだけ。このページで注意されているのはDBを開く2メソッドのみ。 実際にDBを触る insert() / query() 等には言及なし Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 25
  19. 第1章 | なぜ作り込むか ② いつもはうまくいってるから大丈夫 スレッド切り替えをちゃんとやるライブラリに慣れて油断する ◼スレッド指定をしなくてもI/O処理をワーカースレッドに逃 がしてくれるライブラリは多い ⚫ Room

    いつも大丈夫だし、 DB操作も大丈夫でしょ ⚫ DataStore ⚫ Retrofit ◼メソッドをそのまま呼び出せばOKの経験が多いと油断す る Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 27
  20. 第1章 | なぜ作り込むか ③ ANRの発生は環境に依存する 開発時の動作確認では気づきにくい ◼ANRの発生を左右する環境要因 ⚫ 端末のスペック ⚫

    データ量 ⚫ そのときユーザーが操作したか ◼開発時点では、発生しないことの方が多い ◼動作確認の時点ではANRが無いのではなく、まだ出ていないだけかもしれない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 28
  21. 第1章 | 処方・再発防止 3つの作り込みやすさに、処方を1つずつ 作り込みやすさ ① 同期APIを選んだ自覚がない ② いつもはうまくいってるから大丈夫 ③

    ANRの発生は環境に依存する 処方 設計:逃がす責務を1箇所に決める 呼び出し側は意識しなくていい状態にする 調査:ライブラリの守備範囲を知る どこまで守ってくれるかはライブラリ次第 検知:ディスクI/Oを StrictMode で見張る 5秒待たずに検知した時点で鳴る Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 29
  22. 第1章 | 処方・再発防止 ① 逃がす責務を1箇所に決める ◼I/O はワーカースレッドに逃がす ◼逃がす責務は Repository が持つ設計でルール化する

    // ✗ 呼び出し側が毎回スレッドを意識する viewModelScope.launch(Dispatchers.IO) { repository.dailyStats() } // ✓ Repository が自分の責任で逃がす(呼び出し側は意識しなくていい) class StatsRepository(private val db: StatsDatabase) { suspend fun dailyStats(): List<DailyStat> = withContext(Dispatchers.IO) { db.queryAll() } } 形が1つに決まっていれば人もAIも真似して書ける Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 30
  23. 第1章 | 処方・再発防止 ② ライブラリの守備範囲を知る ◼ライブラリによって、メインスレッドから呼んだときの守られ方が違う 守り方 例 アプリ側でやること 内部で逃がす

    Room の suspend DAO そのまま呼ぶだけ 実行時に落とす ネットワーク / Room の同期DAO 何もしない 生の SQLite / SQLDelight 自分で逃がす 忘れたら落ちるので気づく 自分で逃がす 忘れても気づけない ◼I/O処理で使うライブラリを決めたら、守備範囲を確認して、チームやAIが迷わない実装ルールに する Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 31
  24. 第1章 | 処方・再発防止 ③ ディスクI/Oを StrictMode で見張る ◼StrictMode = アプリの実装ミスを実行時に検出する

    Android 標準の仕組み ◼StrictMode は5秒待たなくてもメインスレッドI/Oが検知された 時点で鳴る // Application.onCreate(デバッグビルドのみ) StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder( StrictMode.getThreadPolicy(), // 既定を引き継ぐ ) .detectDiskReads() .detectDiskWrites() .penaltyLog() .build() ) メインスレッドでのI/Oを通知するデバッグ機能 ※ DorodoroTimer の実装より(core/debug/StrictModeInstaller.kt) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 32
  25. 第1章 | まとめ 第1章のまとめ 仕組み メインスレッドが詰まったまま、入力に5秒応えられないとANR 診断 まず分析情報、次にトレースを下に遡って自分のコードに当たる 原因 スレッドを管理しないライブラリの

    I/O を、メインでそのまま呼んでいた 作り込み 選んだ自覚がない / いつもうまくいっていた / 環境に依存する 処方 設計=責務を1箇所に / 調査=守備範囲を知る / 検知=StrictMode 処方で防げるのはこれから書くコード。潜んでいるANRは環境が変わると噴き出す Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 33
  26. 第2・3章 | きっかけ 増えたANRの種類はバラバラ ANR 分析情報 メインスレッドの I/O 診断 傾向

    第1章で診断済み 急増 Binder 呼び出しの遅延 急増 ClassLoader 遅延 急増 ロックの競合 急増 ◼件数が増えたではなく、それまであまり見なかったANRが大量にでていた Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 36
  27. 第2・3章 | きっかけ 特徴① トレース トレースをたどると Application.onCreate() ◼ロック競合も Binder もメインスレッドI/Oも、トレースをたどると同じ場所に着く

    "main" tid=1 Runnable state=R utm=483 #09 java.security.MessageDigest.digest #11 ….app.startup.StartupWork.hashChain #28 ….app.startup.PerformanceMonitorInitializer.init #36 ….app.DorodoroApplication.onCreate #40 android.app.Instrumentation.callApplicationOnCreate #44 android.app.ActivityThread.handleBindApplication ※ DorodoroTimer ANR-02 から取得したトレース Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 37
  28. 第2・3章 | きっかけ 起動経路が増えていた ◼バックグラウンドで動く機能 アプリを閉じていても、OSがプロセスを起こして呼び出す ⚫ BroadcastReceiver: OSからのイベント通知など ⚫

    WorkManager: 定期ジョブ ◼Deep link 流入 リンクのタップから、アプリを起動して任意の画面へ直行する いずれも、プロセスが居なければOSが起こす = onCreate から始まる Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 40
  29. 第2章 | きっかけ Binder 呼び出しの遅延 ANR 分析情報 診断 傾向 メインスレッドの

    I/O 第1章で診断済み 急増 Binder 呼び出しの遅延 この章で診断 急増 ClassLoader 遅延 急増 ロックの競合 急増 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 42
  30. 第2章 | きっかけ 起動時に頻発する Binder 呼び出し遅延 ◼Application.onCreate()で呼び出してる処理で頻発する Binder 呼び出し遅延 ◼起動経路が増えた

    → 冷えた状態からの起動=初期化が走る回数が増えた ◼アプリ初期化はBinder 呼び出しを本質的に多く含む ◼トレースの main は BinderProxy.transact という見慣れない行で止まっていた Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 43
  31. 第2章 | 基礎 Binder 呼び出しとは ◼プロセスは互いに隔離されている(別メモリ)= 相手のメソッドを直接呼べない ◼プロセス間の壁を越える通路が Binder シス

    ム ー ス eystore 自分のアプリ eM er 別アプリ M i スレッド(呼ぶ側) アプリの er i e 自分の別プロセス i der droid pro ess ◼Binder は「別プロセスのメソッド呼び出し」を実現するためのIPCの仕組み Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 44
  32. 第2章 | 基礎 Binder 呼び出しの中で起きていること 自分のプロセス 手のプロセス ro y t

    i der (実体) ◼呼び出し側からは 普通のメソッド呼び出しに見える ◼実際には xy → B nd → ub を経由している Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 45
  33. 第2章 | 基礎 Binder が遅いとANRになるのは呼んだ側 呼び出し元のメインスレッドが返事を待って止まる 切 自分のアプリ メインスレッド ANR

    返事待ち 再開 呼ぶ 手のプロセス 処理中 (遅い) ◼相手のプロセスの処理が遅いほど、メインスレッドの待機時間も長くなる ◼待機中は描画や入力処理も進められない ◼ANRの条件を満たすと、自分のアプリがANRになる Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 46
  34. 第2章 | トレースの読み方 BinderProxy.transact で止まっている Subject: Process ProcessRecord{… dorodorotimer …}

    failed to complete startup "main" tid=1 Native state=S utm=32 ← CPU 0.4秒=働いていない at android.os.BinderProxy.transact(BinderProxy.java:676) at ….ISecureVault$Stub$Proxy.generateKey(ISecureVault.java:118) at ….SecureVaultBootLoader.loadKeyBlocking(SecureVaultBootLoader.kt:76) at ….app.DorodoroApplication.onCreate(DorodoroApplication.kt:72) at android.app.ActivityThread.handleBindApplication(…:8404) ◼main は Native・CPU 0.4秒 = 働いていないのに止まっている ◼最上段は BinderProxy.transact = 壁の向こうの返事待ちそのもの Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 47
  35. 第2章 | トレースの読み方 呼び口は普通のメソッドに見える Subject: Process ProcessRecord{… dorodorotimer …} failed

    to complete startup "main" tid=1 Native state=S utm=32 ← CPU 0.4秒=働いていない at android.os.BinderProxy.transact(BinderProxy.java:676) at ….ISecureVault$Stub$Proxy.generateKey(ISecureVault.java:118) at ….SecureVaultBootLoader.loadKeyBlocking(SecureVaultBootLoader.kt:76) at ….app.DorodoroApplication.onCreate(DorodoroApplication.kt:72) at android.app.ActivityThread.handleBindApplication(…:8404) ◼コードに書いてあるのは generateKey() = ただのメソッド呼び出し ◼実体は $Stub$Proxy = 壁越し通信を隠す代理(AIDL生成) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 48
  36. 第2章 | トレースの読み方 手プロセスはトレースには出てこない ◼Binder の 手プロセスのスタックは、ANRトレースからは分からない ⚫ ダンプされるのは ANRを起こした自分のプロセスだけ

    ----- dumping pid: 11431 ← ANRを起こした自分 thread 11431: l 10 need_return 0 tr 0 outgoing transaction … from 11431:11431 elapsed 11038ms to 11469:11484 … ※ AOSP(Pixel エミュレータ / Android 17)のANRレポートより ◼分かるのは 宛先のPIDと待ち時間 だけ Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 49
  37. 第2章 | トレースの読み方 起動時ANRの特徴 ◼起動が完了しないプロセスは、bindApplication の 切でOSに見限られる 入力ディスパッチ 起動(bindApplication) 5秒(公式doc)

    約15秒(AOSP実装値+実測) 時計の起点 入力が届いてから プロセス起動の瞬間から無条件 場面 前面(操作中) 背面でも ユーザー 「応答していません」ダイアログ 何も出ない(無言 kill) トレースの Reason Input dispatching timed out failed to complete startup 切 ユーザーには何も見えない。それでも Crashlytics / Vitals には残る Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 50
  38. 第2章 | 事例 Application.onCreate でよく見た Binder 呼び出し遅延 ◼FirebaseApp.initializeApp ⚫ PackageManager

    ⚫ ActivityManager ◼MapsInitializer.initialize ⚫ GMS(Google Maps)プロセス ◼どちらもぱっと見はよくある初期化コード Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 51
  39. 第2章 | 事例 Firebase の初期化 ◼Application.onCreate の FirebaseApp.initializeApp の中で PackageManager

    / ActivityManager への Binder 待ち ◼初期化のコードの追加は不要だった ◼Google 公式サンプル(Now in Android)も Firebase を使っているのに、onCreate に初期 化の行は無い For a vast majority of apps, FirebaseInitProvider will handle the initialization of Firebase for the default project that it's configured to work with(中略)it runs automatically at app launch. No additional lines of code are needed in this case. firebase-android-sdk — FirebaseApp.java Javadoc Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 52
  40. 第2章 | 事例 Firebase の初期化は Provider ◼FirebaseInitProvider.onCreate() が FirebaseApp.initializeApp を実行

    ⚫ Application.onCreate() から初期化コードと同等 ⚫ 重い処理はその先にある ◼Provider も Application.onCreate() も同じ起動予算(15秒)内で処理される ◼Firebase を入れているアプリには起動時に避けられない Binder 呼び出しがある ◼アプリ起動時の処理は本質的に Binder 呼び出しが多い Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 53
  41. 第2章 | 事例 Maps SDKの初期化 ◼MapsInitializer.initialize の中で GMSプロセスへの Binder 待ち

    ◼この行は、公式「新しい地図レンダラ」ページの実装例に従っただけ internal class MapRendererOptInApplication : Application(), OnMapsSdkInitializedCallback { override fun onCreate() { super.onCreate() MapsInitializer.initialize(applicationContext, Renderer.LATEST, this) } // onMapsSdkInitialized( の受け取り)は省略 } ※ Maps SDK for Android — 新しい地図レンダラ(2022-12-06 版・Web Archive) の Kotlin 実装例 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 54
  42. 第2章 | 事例 要件はマップ生成前の初期化 ◼Application.onCreate() は必須ではない。表示の前に初期 化すればOK ◼バックグラウンドでのプロセス起動時は不要 ⚫ バックグラウンド時はMapの表示のしようがない

    MapView、MapFragment、または SupportMapFragment が作成される前に、コードで MapsInitializer.initialize() を呼び出す必要があります。アプリのコンテンツ ビューが設定さ れる前に、Application または Activity の onCreate 内でこれを呼び出すことをおすすめし ます。 Maps SDK for Android — 新しい地図レンダラ(公式・日本語版) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 55
  43. 第2章 | 原因 あなたの Binder 呼び出し遅延はどこから? Firebase Maps SDK 入り口

    FirebaseApp.initializeApp() MapsInitializer.initialize() 待ち先 PackageManager(Provider の探索) GMSプロセス その1行を消すと FirebaseInitProvider がすでに 済ませている(onCreate の前) 地図を出さない起動なら不要 起動時の Binder 必須なので消えない 使う前に初期化で良いので 消せる ◼起動時処理であえてやる必要の無い Maps SDKの初期化をしていた Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 56
  44. 第2章 | 処方 動かせるものだけ動かす ◼動かせない処理はそのまま: 起動時のコストとして計上する ⚫ FirebaseInitProvider の Binder

    呼び出し ◼動かせる処理は動かす: 初回フレームより後ろへ ⚫ Maps SDKの初期化のような Binder 呼び出し ⚫ Binder 呼び出しに限らず、後でよい処理も同じ Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 58
  45. 第2章 | 処方 起動時の ClassLoader 遅延を避ける ◼メインスレッドでクラスの初回ロード・初期化が走ると、起動処理がその間止まる ◼DIで必要なクラスをまとめて解決すると、クラスロードも起動時に集中しやすい ◼起動に要らない定義は lazyModule

    に分け、裏のコルーチンで登録する val analyticsModule = lazyModule { single<AnalyticsService>() } val reportingModule = lazyModule { single<CrashReporter>() } startKoin { // Load critical modules immediately modules(coreModule, networkModule) // Load non-critical modules in background lazyModules(analyticsModule, reportingModule) // 既定は Dispatchers.Default } Koin 公式「Lazy Modules and Background Loading」 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 59
  46. 第2章 | 処方 Binder 呼び出しはメインスレッドの外へ ◼Binder 呼び出しはワーカースレッド実行推奨 ◼自アプリのコードから Binder 呼び出しする場合はワーカースレッドで実行する

    デフォルトでは、IPC 呼び出しは同期的です。サービスがリクエストを完了するのに数ミリ秒以上かかることが分かっている場合は、 アクティビティのメインスレッドから呼び出さないでください。アプリがハングし、Android に「アプリケーション応答なし」ダイアログが表 示される可能性があります。クライアントの別のスレッドから呼び出します。 Android Developers — Android インターフェース定義言語(AIDL)(「インターフェースを実装する」節) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 60
  47. 第2章 | まとめ 第2章のまとめ 仕組み プロセスをまたぐ同期呼び出し。相手の応答を main で待ち、予算超過でANR 診断 呼ばれた側は写らない。BinderProxy.transact

    と Binder Transaction Info を読む 事例 onCreate の初期化1行(Firebase・Maps)。手順書どおりの実装でもANR 作り込み 消せない行と、起動時にやらなくていい行が同じ場所に積み重なる 処方 後回し=今要らない初期化 / main の外へ=Binder・クラスロード Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 61
  48. 第3章 | きっかけ 「ロックの競合」 ANR 分析情報 診断 傾向 メインスレッドの I/O

    第1章で診断済み 急増 Binder 呼び出しの遅延 第2章で診断済み 急増 ClassLoader 遅延 第2章で診断済み 急増 ロックの競合 この章で診断 急増 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 63
  49. 第3章 | 基礎 ロックとは ◼閉じ込めない共有データは鍵で守る = ロック(代表例: synchronized) wor er

    A i wor er wor er A i wor er 鍵(wor er A が使用中) 共有データ るのはメインスレッドだけ(第 章・ ) 同時に れるのは つだけ、 は待つ(この章) ◼鍵が使用中なら空くまで待つので、メインスレッドの場合ANRのリスクがある Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 65
  50. 第3章 | トレースの読み方 「働きすぎ」or「待たされてる」 main の状態とCPU時間から見分ける 見るところ 働きすぎ 待たされてる(この章) スレッド状態(ART)

    Runnable Blocked state(Linux) R=実行中 S=待機状態(sleeping) CPU時間 utm 大きい(例: 483 ≒ 4 8秒) 小さい(例: 136 ≒ 1 4秒) ※ 下2行は生のスタックトレース(bugreport / ApplicationExitInfo)でのみ見える。Crashlytics・Vitals では落ちる ◼CPU時間が小さいのに止まっている = メインスレッドは働いてないのに動けない ◼何を待っているのかをスタックトレースから読み解く Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 69
  51. 第3章 | トレースの読み方 待たされてるメインスレッド "main" tid=1 Blocked state=S utm=136 ←

    CPU 1.4秒=働いていない at ….stats.StatsStore.awaitReady(StatsStore.kt:149) - waiting to lock <0x0920c282> (a ...StatsStore) held by thread 32 at ….stats.StatsScreenKt$StatsScreen$1$1.invokeSuspend(…:53) at android.os.Handler.dispatchMessage(Handler.java:125) at android.os.Looper.loop(Looper.java:393) DorodoroTimer 生のスタックトレースを抜粋して整形 ◼main は Blocked・CPU 1.4秒 = 「待たされてる」側 ◼held by thread 32 = 鍵を握ってるのはスレッド32。そのスタックを探す Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 70
  52. 第3章 | トレースの読み方 メインスレッドを待たせているスレッド32 "stats-store-warmup" tid=32 Runnable state=R utm=1808 ←

    CPU 18秒 at java.security.MessageDigest.digest(MessageDigest.java:425) at ….stats.StatsStore.heavyInitWork(StatsStore.kt:167) at ….stats.StatsStore.warmUp(StatsStore.kt:135) - locked <0x0920c282> (a ...StatsStore) ← main と同じアドレス at ….app.DorodoroApplication.onCreate$lambda$0(…:56) at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) ◼main と同じアドレスのロックを握って Runnable・CPU 18秒の長仕事中 ◼ワーカースレッドに逃がしてもメインスレッドが同じ鍵をとりに行くと待たされることがある Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 71
  53. 第3章 | 事例 Maps SDKで地図を出すだけでANRになる ◼main は ContextImpl.getExternalFilesDir 待ちで Blocked

    ◼作り込んだのはアプリではない ⚫ アプリ側のコードは地図を表示しているだけ ◼発生条件は読めない ⚫ 出る端末と出ない端末があり、手元では再現しない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 72
  54. 第3章 | 事例 ストレージのディレクトリ操作で鍵を取る M ps のワーカー M pView o

    re te et ter l iles ir ディレクトリの確認・作成(e ists dirs rite) したときだけ old へ i der(別プロセス) ◼名前は get だが、実体は用意して返す ⚫ 無ければ作る(mkdirs)/書けるか確かめる(canWrite) ◼同時に呼ばれても競合しないようにContext の鍵を握っておく Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 73
  55. 第3章 | 事例 メインスレッドも同じ鍵を取りに行く M ps のワーカー M pView o

    re te et ter i l iles ir et ter l iles ir で待つ( lo ed) ANR ◼メインスレッドも同じ getExternalFilesDir を呼ぶ ⚫ MapView / SupportMapFragment から ◼鍵が空くまで メインスレッドは Blocked ⚫ 入力を捌けないまま5秒でANR Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 74
  56. 第3章 | 原因 あなたのロック競合はどこから? ◼実装自体は妥当。ロックは正常な動作で、1つの diff にも写らない ◼SDK自身がワーカースレッドに逃がしたのは第1章の処方どおり。それでもメインスレッドは同じ鍵で 止まる ◼鍵の中では全ボリュームをループ(内蔵+SDカード)

    ⚫ 端末によっては数秒まで伸びる ◼鍵に触っているのはワーカースレッドもメインスレッドも SDK自身 SDK側の挙動でそうなってるので、自アプリはどうしようもない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 75
  57. 第3章 | 処方(自分の鍵) sy hro ized → Mutex.withLock // Before(ANR版):

    鍵が塞がっていると、main はスレッドごと止まる(Blocked) fun awaitReady() = synchronized(this) { /* ... */ } // After(一次対処): 待っても main は suspend するだけ。描画も入力も止まらない suspend fun awaitReady() = mutex.withLock { /* ... */ } ◼差分はほぼ1行。ANRはこれで消える ◼ただし凍らない ≠ 速い 鍵を25秒握る構造は残る 「Kotlin と Compose を使っている場合、プリミティブなロックはノンブロッキングなコルーチンの Mutex(Mutex.withLock)に置 き換えられます。UI スレッドを凍らせる代わりに実行コンテキストを suspend することで、スレッドのブロックを防ぎます。」/ 出典: Android Developers — ANR(英語版・日本語訳) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 76
  58. 第3章 | 処方(自分の鍵) そもそも待たない: StateFlow で観測する private val _isReady =

    MutableStateFlow(false) val isReady: StateFlow<Boolean> = _isReady.asStateFlow() suspend fun warmUp() { withContext(Dispatchers.Default) { heavyInitWork() } _isReady.value = true } // UI側: warmUp() の完了は await しない // isReady を観測して Loading → Ready を描くだけ // 重い仕事は鍵の外 // 完了は Flow で流す ◼同期待ちAPI(awaitReady)は廃止して、UIは準備状態を観測して描くだけ ◼メインスレッドが塞がる瞬間が構造ごと消える(Loading 中も画面は生きている) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 77
  59. 第3章 | まとめ 第3章のまとめ 仕組み BGが鍵を握ったまま長仕事 → 診断 main は

    Blocked・CPU時間小=被害者。held by を辿って犯人へ 事例 フレームワークの鍵でも起きる(Maps SDK)。SDK自身が第1章の処方どおり逃がしても、鍵が同じなら 意味がない 作り込み 悪い1行は無い。「長い保持 × main の同期待ち」は diff に写らない 処方 n が同じ鍵で止まる → 入力5秒でANR 人の鍵は直せない / 自分の鍵は凍らせない=Mutex・待たない=StateFlow Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 78
  60. 終章 | 運用の目標 アプリ側で防げないANR ◼ANRの発生は環境に依存する ⚫ 端末のスペックが低い ⚫ シス ム全体が高負荷

    ⚫ 扱うデータが増えると同じコードでも 処理にかかる時間が伸びる(第1章 メインスレッドの I/O) ⚫ getExternalFilesDir はボリュームの本数分回る (第3章 ロックの競合) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 81
  61. 終章 | 運用の目標 ゼロは目標にしない ◼求められているのはANRゼロではない ⚫ しきい値は0.47%(全端末)/ 8%(単一機種) ◼Crashlytics や

    Vitals を確認して多い順に対処で十分 ⚫ 全端末でしきい値を下回っていても 特定の機種で踏み抜くことがあるのは注意 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 82
  62. 終章 | 持ち帰り あなたのANRはどこから? ◼答えはスタックトレースにある ⚫ 最上段の I/O(第1章)/ プロセスの外(第2章)/ held

    by の先(第3章)…など 帰ったら Crashlytics や Vitals を開いて、ANRをチェック 「これ、もしかしたらあのパターンかも?」とピンと来たら、 今日の話が血肉になった証拠です Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 83
  63. 付録 参考資料 公式ドキュメント・一次情報 ◼ANR(Android vitals) / ANR を診断して修正する ◼アプリの応答性を維持する /

    アプリの起動時間 ◼プロセスとスレッド / AIDL: インターフェースの実装 ◼Android vitals の不適切な動作のしきい値(Play Console ヘルプ) ◼技術品質の基準を引き上げる(Android Developers Blog・2022) ◼新しい地図レンダラ(Maps SDK for Android) / 当時の版 ◼Room の非同期クエリ / SQLite でのデータ保存 ◼FirebaseApp.java L68(自動初期化の Javadoc) / Koin「Lazy Modules」 Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 84