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

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

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

# やったこと
### 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」より、クラッシュレポートが確認できます
こんな感じで、かなりの数が報告されています

![スクリーンショット 2021-03-27 15.58.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/ac2e97fb-ffbb-ee79-0001-f2822ab84f43.png)

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

## 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端末固有のフォーム長押し問題](https://issuetracker.google.com/issues/37127697)かと思いましたが、微妙に違うように見えます
（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系の問題](https://stackoverflow.com/questions/45840856/android-8-0-oreo-crash-on-focusing-textinputedittext)のようで、実際Android8の3デバイスで発生していました
リンク先の記事を追うとわかりますが、こちらもTextInputEditText関連です

### 処置

[こちらのブログ記事](https://dashimaki-dofu.hatenablog.com/entry/2018/09/24/175307)や上記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@210214081@21.02.14 (120400-0):1)
  at com.google.android.gms.maps.internal.q.aX (com.google.android.gms.dynamite_mapsdynamite@210214081@21.02.14 (120400-0):12)
  at dt.onTransact (com.google.android.gms.dynamite_mapsdynamite@210214081@21.02.14 (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の公式ドキュメント](https://developers.google.com/maps/documentation/android-sdk/map?hl=ja)で言われているように、ライフサイクルと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のクラッシュレポートに対して自分なりに対策を入れてみました
入力フォームやマップあたりがやや不安定な印象ではありますね
一旦これで様子をみてみようかと思います