【Android】AsyncTask地獄にハマったあの日 〜非同期ってそういう意味じゃないんだよ…〜

バックグラウンドで処理すればサクサクになる!
そう信じて使い始めたAsyncTaskが、アプリをカクカクにし、バグだらけにし、やがて自分をも破壊するとは思いもしませんでした。
今回は、非同期処理の基礎を知らなかった私が、AsyncTaskに振り回され、そしてようやく抜け出すまでの失敗談をお届けします。

非同期って、楽になるんでしょ?

Javaでの開発経験がある程度あった私は、Androidアプリでも「スレッド使えばいいんでしょ」と軽く考えていた。
でも、UIスレッドで重い処理をするとクラッシュすることを知り、救世主のように登場したのが AsyncTask だった。

new AsyncTask<Void, Void, String>() {
    @Override
    protected String doInBackground(Void... voids) {
        // 重い処理
        return "完了";
    }

    @Override
    protected void onPostExecute(String result) {
        // UI更新
        textView.setText(result);
    }
}.execute();

初めて見たときは「めっちゃ便利じゃん!」と思った。
でも、これがまさか、最悪の選択になるとは思いもせず…。

地獄の入り口:なぜか画面が固まる

当時私は、画面遷移時に複数のAPIを順番に呼び出す処理を作っていた。
それぞれをAsyncTaskでラップして、onPostExecute()で次のAsyncTaskを呼ぶようにした。

// API1 → API2 → API3 の順に呼ぶ
new ApiTask1().execute();

class ApiTask1 extends AsyncTask<Void, Void, Data1> {
    protected Data1 doInBackground(...) { ... }
    protected void onPostExecute(Data1 result) {
        new ApiTask2().execute();
    }
}

一見すると問題なさそうに見える。でも実際は、

  • 複数のタスクが並列実行されてしまい、予期しない順番で完了
  • 画面が遷移中にキャンセルされずクラッシュ
  • 一部タスクが Activity破棄後にもUIにアクセスして例外発生

と、もうグッチャグチャになってしまった。

本当の地獄:Activityリークとメモリクラッシュ

やらかした中でも最悪だったのが、非staticなAsyncTaskの内部クラスActivityを持たせてしまったこと。

class MyActivity extends Activity {
    class MyTask extends AsyncTask<Void, Void, Void> {
        protected Void doInBackground(...) {
            // MyActivity.this を参照
        }
    }
}

このコード、画面回転やバックキーでActivityが破棄されても、非同期処理が終わるまでメモリが解放されないという恐ろしいバグを引き起こす。
ひどいときは、数分でOOM(Out of Memory)になってアプリが強制終了。
しかも、原因を突き止めるのに数週間かかった。

じゃあどうすればよかったのか?

当時の私に教えてあげたいこと:

  • AsyncTaskは短命のUI補助用と割り切るべし
  • 複数のAPIコールを直列化するなら、状態を管理するロジックを作るべき
  • Activityに強く依存する処理は、非staticクラスでは書かない
  • 重い処理はIntentServiceや後のWorkManagerなどを使うべきだった
  • 今なら絶対に Coroutines(Kotlin)かRxJava を使ってた…

そして、脱・AsyncTaskへ

2018年、GoogleがAsyncTaskの非推奨を発表したとき、私は思った。
「やっとあいつが歴史から葬られた…」

それ以降、私はKotlinとCoroutinesに移行。suspend関数やViewModelScopeの登場によって、非同期処理は圧倒的にシンプルで安全になった。

viewModelScope.launch {
    val data1 = repository.loadData1()
    val data2 = repository.loadData2()
    _uiState.value = UiState.Success(data1, data2)
}

今でも時々、「あの頃の俺、なぜあんなにがむしゃらにAsyncTaskに頼ったのか」と思う。
それしか手段を知らなかった、それだけだった。

おわりに

失敗を経て学ぶ。とは言うけれど、AsyncTaskに関しては、できれば通らずに済む道であってほしかった。

でも、あの経験がなければ、今の堅牢な非同期設計力はなかったかもしれない。
過去の自分に、あえてこう言いたい。

「そのAsyncTask、本当に必要か?」と。

タイトルとURLをコピーしました