エラーが発生します。 アプリがフォアグラウンドにある場合、(中略)ユーザーにダイアログが表示されます。ANR ダ イアログにより、ユーザーはアプリを強制終了できます。 Android Developers — ANR Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 2
◼Web:ページ無応答 ⚫ Chrome の「このページは応答しません(待機/終了)」 ◼ダイアログ出して待つ/閉じるをユーザーに選択させるANRは Android 独自のもの Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 3
Play で見つかりにくくなる可能性があります。アプリの不正な動作が特定の デバイスモデルで発生する場合、Google Play は、そのデバイスでは該当するタイトルの使用をやめて、より適した別のアプリを使 用するようユーザーを誘導します。 Android Vitals — Core technical quality(不正な動作のしきい値) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 5
場合によっては、ユーザーが事前に認識できるようアプリのストアの掲載情報 に警告が表示され、技術的品質の高い別のアプリを探すためのオプションが 提供されることもあります。 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
普段はKMPで、担当は View〜ViewModel 層だけ ⚫ 実装の中でスレッド管理を意識することがなかった ◼自分の担当範囲を超えて、Repository 層まで深掘りすることにした Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 14
◼コルーチンでは、どのスレッドで動かすかを「ディスパッチャ」が決める ⚫ Dispatchers.IO:待つ仕事用。待つ間スレッドは暇なので、多めに持てる(既定 最大64) ⚫ Dispatchers.Default:計算する仕事用 CPUコア数に応じた並列速度で動くため、スレッドを増やしても大幅には早くならない Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 17
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
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
ANRの発生は環境に依存する 処方 設計:逃がす責務を1箇所に決める 呼び出し側は意識しなくていい状態にする 調査:ライブラリの守備範囲を知る どこまで守ってくれるかはライブラリ次第 検知:ディスクI/Oを StrictMode で見張る 5秒待たずに検知した時点で鳴る Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 29
第1章で診断済み 急増 Binder 呼び出しの遅延 急増 ClassLoader 遅延 急増 ロックの競合 急増 ◼件数が増えたではなく、それまであまり見なかったANRが大量にでていた Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 36
WorkManager: 定期ジョブ ◼Deep link 流入 リンクのタップから、アプリを起動して任意の画面へ直行する いずれも、プロセスが居なければOSが起こす = onCreate から始まる Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 40
→ 冷えた状態からの起動=初期化が走る回数が増えた ◼アプリ初期化はBinder 呼び出しを本質的に多く含む ◼トレースの main は BinderProxy.transact という見慣れない行で止まっていた Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 43
ム ー ス 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
返事待ち 再開 呼ぶ 手のプロセス 処理中 (遅い) ◼相手のプロセスの処理が遅いほど、メインスレッドの待機時間も長くなる ◼待機中は描画や入力処理も進められない ◼ANRの条件を満たすと、自分のアプリがANRになる Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 46
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
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
⚫ ActivityManager ◼MapsInitializer.initialize ⚫ GMS(Google Maps)プロセス ◼どちらもぱっと見はよくある初期化コード Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 51
/ 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
呼び出し ◼動かせる処理は動かす: 初回フレームより後ろへ ⚫ Maps SDKの初期化のような Binder 呼び出し ⚫ Binder 呼び出しに限らず、後でよい処理も同じ Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 58
デフォルトでは、IPC 呼び出しは同期的です。サービスがリクエストを完了するのに数ミリ秒以上かかることが分かっている場合は、 アクティビティのメインスレッドから呼び出さないでください。アプリがハングし、Android に「アプリケーション応答なし」ダイアログが表 示される可能性があります。クライアントの別のスレッドから呼び出します。 Android Developers — Android インターフェース定義言語(AIDL)(「インターフェースを実装する」節) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 60
と Binder Transaction Info を読む 事例 onCreate の初期化1行(Firebase・Maps)。手順書どおりの実装でもANR 作り込み 消せない行と、起動時にやらなくていい行が同じ場所に積み重なる 処方 後回し=今要らない初期化 / main の外へ=Binder・クラスロード Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 61
A i wor er wor er A i wor er 鍵(wor er A が使用中) 共有データ るのはメインスレッドだけ(第 章・ ) 同時に れるのは つだけ、 は待つ(この章) ◼鍵が使用中なら空くまで待つので、メインスレッドの場合ANRのリスクがある Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 65
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
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
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
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
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
⚫ 扱うデータが増えると同じコードでも 処理にかかる時間が伸びる(第1章 メインスレッドの I/O) ⚫ getExternalFilesDir はボリュームの本数分回る (第3章 ロックの競合) Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 81
by の先(第3章)…など 帰ったら Crashlytics や Vitals を開いて、ANRをチェック 「これ、もしかしたらあのパターンかも?」とピンと来たら、 今日の話が血肉になった証拠です Copyright(C) Nomura Research Institute, Ltd. All rights reserved. 83