Androidアプリのクラッシュ対策にやったこと


先日GooglePlayにAndroidアプリをリリースしたのですが、1週間でおよそ20件のクラッシュレポートが上がってきました
レポートは20件ほどありましたが、およそ2件の対策をしましたのでメモしておきます

解決の保証はできませんので悪しからず

やったこと

TextInputEditTextの実装修正

  • noExcludeDescendantsを追加
  • hint属性はTextinputLayoutにつける
  • setOnEditorActionListenerを削除

MapViewのnullチェックを追加

MapActivity.kt
override fun onDestroy() {
        try { 
            binder?.mapView?.onDestroy()
        } catch (e : NullPointerException) {
            // Send report exception..
        }
        super.onDestroy()
    }

クラッシュレポート

GooglePlayConsoleのサイドバー「障害およびANR」より、クラッシュレポートが確認できます
こんな感じで、かなりの数が報告されています

たくさん出ていて目眩がしそうですが、対処できそうなのはおおよそ2種類に収まりました

TextInputEditText関連

setHint周りの例外

java.lang.NullPointerException: 
  at android.widget.Editor.stopTextActionMode (Editor.java:2687)
  at android.widget.Editor.prepareCursorControllers (Editor.java:822)
  at android.widget.TextView.nullLayouts (TextView.java:9153)
  at android.widget.TextView.checkForRelayout (TextView.java:10101)
  at android.widget.TextView.setHintInternal (TextView.java:6534)
  at android.widget.TextView.setHint (TextView.java:6523)
  at com.google.android.material.textfield.TextInputLayout.dispatchProvideAutofillStructure (TextInputLayout.java:1280)
  at android.view.ViewGroup.dispatchProvideAutofillStructure (ViewGroup.java:3774)
  at android.view.ViewGroup.dispatchProvideAutofillStructure (ViewGroup.java:3774)
  at android.app.assist.AssistStructure$WindowNode.<init> (AssistStructure.java:517)
  at android.app.assist.AssistStructure.<init> (AssistStructure.java:2047)
  at android.app.ActivityThread.handleRequestAssistContextExtras (ActivityThread.java:3386)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1899)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7050)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:965)

Samsung端末固有のフォーム長押し問題かと思いましたが、微妙に違うように見えます
(performLongClickとかが絡んでいない)
とはいえ今回クラッシュしたのもSamsung系端末でした

AssistStructure$WindowNodeの例外

java.lang.NullPointerException: 
  at android.app.assist.AssistStructure$WindowNode.<init> (AssistStructure.java:484)
  at android.app.assist.AssistStructure.<init> (AssistStructure.java:1908)
  at android.app.ActivityThread.handleRequestAssistContextExtras (ActivityThread.java:3040)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1812)
  at android.os.Handler.dispatchMessage (Handler.java:105)
  at android.os.Looper.loop (Looper.java:251)
  at android.app.ActivityThread.main (ActivityThread.java:6563)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:240)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:767)

こちらはAndroid8系の問題のようで、実際Android8の3デバイスで発生していました
リンク先の記事を追うとわかりますが、こちらもTextInputEditText関連です

処置

こちらのブログ記事や上記StackOverFlowでも言及されていますが、

  • フォームの自動入力Autofillを無効化(noExcludeDescendants)
  • hintを挿入する位置を TextInputEditText -> TextInputLayout に入れ替える

を実施しました。入力フォーム周りはちょっと不安定なんですかねー

加えて、あまり関係ないかもですがsetOnEditorActionListenerを無効化しました
ユーザ情報入力フォームが今回問題だったのですが、キーボードのエンターボタンが使えなくともUXに大きな影響はないと判断しました

MapView関連

java.lang.NullPointerException: 
  at com.google.maps.api.android.lib6.impl.cy.f (com.google.android.gms.dynamite_mapsdynamite@[email protected] (120400-0):1)
  at com.google.android.gms.maps.internal.q.aX (com.google.android.gms.dynamite_mapsdynamite@[email protected] (120400-0):12)
  at dt.onTransact (com.google.android.gms.dynamite_mapsdynamite@[email protected] (120400-0):4)
  at android.os.Binder.transact (Binder.java:914)
  at com.google.android.gms.internal.maps.zza.zzb (Unknown Source:20)
  at com.google.android.gms.maps.internal.zzk.onDestroy (Unknown Source:26)
  at com.google.android.gms.maps.MapView$zza.onDestroy (Unknown Source:34)
  at com.google.android.gms.dynamic.DeferredLifecycleHelper.onDestroy (Unknown Source:71)
  at com.google.android.gms.maps.MapView.onDestroy (Unknown Source:40)
  at com.example.android.fragment.ExmapleFragment.onLowMemory (LastTripFragment.kt:231)
  at androidx.fragment.app.Fragment.performLowMemory (Fragment.java:3069)
  at androidx.fragment.app.FragmentManager.dispatchLowMemory (FragmentManager.java:3152)
  at androidx.fragment.app.Fragment.performLowMemory (Fragment.java:3070)
  at androidx.fragment.app.FragmentManager.dispatchLowMemory (FragmentManager.java:3152)
  at androidx.fragment.app.FragmentController.dispatchLowMemory (FragmentController.java:379)
  at androidx.fragment.app.FragmentActivity.onLowMemory (FragmentActivity.java:332)
  at android.app.ActivityThread.handleLowMemory (ActivityThread.java:6002)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1921)
  at android.os.Handler.dispatchMessage (Handler.java:107)
  at android.os.Looper.loop (Looper.java:359)
  at android.app.ActivityThread.main (ActivityThread.java:7418)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:492)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:935)

GoogleMapを表示するMapViewと、それに関連するFragmentのライフサイクル系で多発しております
この例ではonLowMemoryですが、onStart, onPause, onDestoryなどでも同様の例外が出ておりました

MapViewの公式ドキュメントで言われているように、ライフサイクルとMapViewを紐付けしていましたが、そのときにMapオブジェクトが存在しないようです

API を完全なインタラクティブ モードで使用している場合は、MapView クラスで、アクティビティ ライフサイクル メソッド(onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy()、onSaveInstanceState()、onLowMemory())を MapView クラスの対応するメソッドに転送する必要があります。

ExampleFragment.kt
override fun onDestroy() {
    binder.mapView.onDestroy()  // ←ここでmapViewがnullになってる
    super.onDestroy()
}

処置

こちらに関しては明確な解決策が見つけられず、とりあえず例外をキャッチすることにしました↓

ExampleFragment.kt
override fun onDestroy() {
        try {
            binder.mapView.onDestroy()
        } catch(e: Exception) {
            /* do nothing */
        }
        super.onDestroy()
    }

これでまずは様子を見てみます

tgkill

backtrace:
  #00  pc 000000000006b970  /system/lib64/libc.so (tgkill+8)
  #00  pc 0000000000068df4  /system/lib64/libc.so (pthread_kill+64)
  #00  pc 0000000000024290  /system/lib64/libc.so (raise+24)
  #00  pc 000000000001ccac  /system/lib64/libc.so (abort+52)
  #00  pc 000000000042cca8  /system/lib64/libart.so (art::Runtime::Abort()+352)
  #00  pc 00000000000e4c24  /system/lib64/libart.so (art::LogMessage::~LogMessage()+1204)
  #00  pc 000000000024ab24  /system/lib64/libart.so (art::IndirectReferenceTable::Add(unsigned int, art::mirror::Object*)+308)
  #00  pc 0000000000324684  /system/lib64/libart.so (art::JNI::FindClass(_JNIEnv*, char const*)+2824)

クラッシュレポートの中でもひときわ異彩を放っているのがこちらで、linuxのシステムコールかな?と思っています
これに関してはスタックトレースがないので原因特定が難しく、またかなり限られたデバイスでしか出ていないので対応を見送っています
(Android7系の古めのデバイスで出るようです)

まとめ

ということで、Androidのクラッシュレポートに対して自分なりに対策を入れてみました
入力フォームやマップあたりがやや不安定な印象ではありますね
一旦これで様子をみてみようかと思います