Fuel で ANR を頻発させる非同期処理の間違った認識|Android Kotlin 実践勉強会
昨今、「Fuel」で WEB 通信を実現している Android アプリも多いようなのですが、先日、お仕事で、速度(パフォーマンス)改善の依頼があったのですが、致命的な速度遅延の原因が、この Fuel の実装ミスでした。
依頼内容は ANR の解消
Android アプリの各画面、その表示速度が、考えられないくらい遅いので、「原因解析」してもらえないか、という依頼でした。とにかく、ANR(Application Not Responding)が頻発するというのです。
「ANR が頻発する」なんて、かな~りの重症ですよ!!!
「速度に致命的な問題」として、コード解析する前に、Android 開発の経験からパッと頭に浮かぶ原因は、以下でした。
・ConstraintLayout を「ネスト」してレイアウト実装してしまった
・何かがメインスレッド(UI スレッド)を猛烈にブロッキングしている
前者はログに警告が表示され、アプリ操作の体感的にも遅いですが、「ANR」が頻発するレベルではありません。十中八九、後者のはずです。
しかし、この企業さん、同時に複数の Android アプリを開発していて、その全てのアプリで、同様の致命的な ANR 現象に悩まされていたそうです。突貫で並行してガンガン開発している状態で、不具合を振り返って調査している時間が無かったそうです。
......恐ろしい現場です。
判明した不具合現象は典型的なメインスレッド(UI スレッド)のブロッキング
実装をこの目で見て、ログを仕込んだら一目瞭然、予想通りの現象でした。
Fuel で REST API と通信していたのですが、軒並み、メインスレッド(UI スレッド)をブロッキングしていました。
1.画面起動
2.メインスレッド(UI スレッド)で Fuel 通信
3.レスポンスがあるまでメインスレッド(UI スレッド)をブロッキング
4.ANR が発生
わぉぉぉぉ、お手本のような不具合でした。
原因は Fuel 非同期の間違った認識
依頼元企業の開発者たちの認識は、
「Fuel は非同期で通信してるけどなあ、httpAsync.join() で」
というものでした。ここに大きな「罠」がありました。以下は、Fuel 公式サイトに掲載されているサンプル「Async Usage Example(非同期の使用例)」です。
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.result.Result
fun main(args: Array<String>) {
val httpAsync = "https://httpbin.org/get"
.httpGet()
.responseString { request, response, result ->
when (result) {
is Result.Failure -> {
val ex = result.getException()
println(ex)
}
is Result.Success -> {
val data = result.get()
println(data)
}
}
}
httpAsync.join()
}
これを Android に置き換えて掲載している WEB サイトがありました。以下です。抜粋します。どうも、開発者たちはこちらを参考にしたらしいのです。
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.result.Result
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val httpAsync = "https://httpbin.org/get"
.httpGet()
.responseString { _, _, result ->
when (result) {
is Result.Failure -> {
val ex = result.getException()
println(ex)
}
is Result.Success -> {
val data = result.get()
val isUiThread = Thread.currentThread() == Looper.getMainLooper().thread
println("IS_TREAD: $isUiThread")
val resulttext = findViewById<TextView>(R.id.resulttext)
resulttext.text = data
}
}
}
httpAsync.join()
}
}
各アプリの、各画面で、上記の実装になっていました。onCreate の同期で Fuel 通信していたのです。
「Async Usage Example(非同期の使用例)」に忠実に従っているので、非同期で通信されていると思われたのかもしれませんが、「httpAsync.join()」が、思いっ切り、メインスレッド(UI スレッド)をブロッキングします。
onCreate(ライフサイクル)をブロッキングするのは Android では御法度です。
「httpAsync.join()」の説明は?
おぉ、コードに書いてあります。
Wait for the request to be finished, error-ed, cancelled or interrupted
(リクエストが終了、エラー、キャンセル、または中断されるのを待ちます)
Google が公式に提供しているライブラリ群を使用していれば、ブロッキングしてしまう実装は、都度、実装時に警告が表示されるのですが、外部のライブラリを使用した場合は、警告が表示されない為、こうやって致命的な不具合を量産してしまう原因になるのです。
なので、私はなるべく、外部のライブラリは使用しない派です。私なら Fuel は使いません。結果的に、未発見で埋没した不具合の対応に追われて、工数が超えてくるからです。まさに本件で、不具合解析を外部に安くない値段で発注する羽目になっていますよね。
Fuel は Coroutines の Suspend ブロックで使用するのが大前提
本件に関する議論が交わされていた海外コミュニティなどの過去ログを覗いてみると、Fuel は Coroutines の Suspend ブロックで使用するのが大前提みたいな発言が多かったです。
ほら、「fuel-coroutines」もあります。
コーヒーブレイク
原因も対応策も判明したので、ちょっとコーヒーブレイク。第 45 回講談社漫画賞にノミネートされていた『青野くんに触りたいから死にたい』が相当に面白いです。
タイトルや表紙から想像したのは、死んでしまった恋人を追い想うような切ない恋物語だったのですが、まさかのホラー漫画。クスッと笑えるし、ホロッと泣けるし、ゾゾッと怖いし。もっともっと人気になる予感。
一番簡単な改修対応
さて、本件の改修対応で、一番簡単なのは、自前で Coroutines の Suspend ブロックを実装することです。
例えば、以下です。今回の不具合検証に用いた、私のサンプル実装です。試してみたい人へ、Android 環境一式も添付します。コメントアウトしている箇所が、本件の原因であるブロッキング実装です。
ここから先は
¥ 390
この記事が参加している募集
この記事が気に入ったらチップで応援してみませんか?