• トップ
  • ブログ一覧
  • android.os.AsyncTaskの正しい使い方
  • android.os.AsyncTaskの正しい使い方

    広告メディア事業部広告メディア事業部
    2019.07.08

    IT技術

    AsyncTaskについて思うこと

    Androidアプリ開発が、一般に広く普及してから「約10年」ほどが経ちました。

    数々のリファレンスが生まれては、内容が古いまま残されている記事が散見されるようになってきました。

    そんな中で、このタイトルにある "AsyncTask"という非同期処理用クラス をとてもよく見かけます。

    APIレベル3以降から使われている非同期処理を担当するクラスの代表で、2019年6月段階では特別にgradleファイルを編集して別途ライブラリを追加する必要がないため、「下手にアプリサイズを増やさず、簡単に実装しやすい」という印象を持ちます。

    今でこそRxJavaRxKotlinAndroid Architecture Componentsに提唱されるLiveData、最近正式採用化されたcoroutinesに取って代わることも多いようです。

    しかし、古くから存在するようなActivityFragmentには、未だに存在し、かつ、昔のノウハウのまま実装されてしまっているものを時折現場で見かけます。

    ここでは、筆者が個人的に開発・リリース・運用を続けているTwitterクライアントを例に、AsyncTaskについて再考していきます。

    AsyncTaskの「良い点」と「悪い点」

    AsyncTaskの良い点

    1. 導入が楽、かつ学習コストが低い(古くからリファレンスが存在するため)
    2. Rxやcoroutinesなどのようにライブラリを追加することなく、AndroidSDKが標準で提供してくれている
    3. 処理時間がそんなにかからない非同期処理などに使いやすい
    4. 使い方さえ間違わなければ手軽

    AsyncTaskの良くないところ

    1. "This AsyncTask class should be static or leaks might occur” 警告を起こしがち
    2. ActivityやFragmentなどの画面から呼ばれる事例が多い
    3. ActivityやFragmentが死んで(onDestroy)もAsyncTaskが生きたままになり、GCされずメモリリークが起きる
    4. しかもAsyncTask内でTextViewなどのViewのUIを操作するというアンチパターンなリファレンスが多い
    5. 画面回転時などIllegal State Exceptionが起きる(Activityの再生成が行われてAsyncTaskが行き場を失い起きる)

    警告について

    AsyncTaskを実装したコードをAndroid Studioで開いた場合、下記のような警告が出ていることがあります。

    1This AsyncTask class should be static or leaks might occur...

    どのようなコードで上記の警告が出るかというと…

    1class TimeLineFragment() : ListFragment() {
    2    private var isSwipeRefresh = false
    3    private var mSwipeRefreshLayout: SwipeRefreshLayout? = null
    4
    5    //(中略)
    6
    7    fun getHomeTimeLineTweet() {
    8        val paging = Paging()
    9        paging.count = GET_MENTION_TWEET_NUM
    10
    11        val task = object : AsyncTask<Void, Void, List<Status>>() {
    12            override fun onPreExecute() {
    13
    14            }
    15
    16            override fun doInBackground(vararg params: Void): List<twitter4j.Status>? {
    17                try {
    18                    return twitter!!.getMentionsTimeline(paging)
    19                } catch (e: TwitterException) {
    20                    e.printStackTrace()
    21                }
    22
    23                return null
    24            }
    25
    26            override fun onPostExecute(result: List<twitter4j.Status>?) {
    27                if (result != null) {
    28                    mAdapter!!.clear()
    29                    for (status in result) {
    30                        mAdapter!!.add(status)
    31                    }
    32                } else {
    33                    showToast(getString(R.string.failed_to_load_timeline))
    34                }
    35                //くるくる消す
    36                mSwipeRefreshLayout!!.clearAnimation() // ここでUIの操作が行われている
    37                mSwipeRefreshLayout!!.isRefreshing = false
    38            }
    39        }
    40        task.execute()
    41    }

    このように、AsyncTaskを匿名クラスとして利用し、なおかつ内部でUIに対する変更を加えている場合に起こります。

    この場合、Fragment(あるいはActivity)が破棄されしまっても、AsyncTaskが生き残ってしまった場合に、UI部品(ここではSwipeRefreshLayout)への参照が残ったままとなり、メモリリークを起こす可能性があります。

    非同期タスクは、あくまでも裏側で実行されていて、処理の完了には時間がかかることが想定されています。

    処理が完了する前にUIコンポーネントが不要になり破棄する必要がある状況になると、本来はUIオブジェクトを破棄するべきところなのに非同期タスクが参照を握っているために破棄されない…といった事が起こります。

    つまり、UIリソースが解放できなくなります。

    こういったコードはどのように修正すればいいのでしょうか?

    AsyncTaskのポイントまとめ

    ポイントを一旦下記にまとめます

    1. AsyncTaskをnon-staticな内部的なクラスにせず、inner classにするか、別クラスにする
    2. WeakReferenceを利用する ←ここが重要!
    3. RefreshCallBackインターフェイスを準備する
    4. APIの取得・更新処理などが終わった時にUI側に結果を返す時に用いる

    コードの修正

    まず、AsyncTaskをディレクトリごと切ります。

    パッケージ右クリック → New → Package

    今回は、そのパッケージ名を「asynctask」とします。

    そのディレクトリで、New → Kotlin File/Classを選択(Javaで開発されている方は以下より.javaに置き換えてください)

    たとえば、ホーム・タイムラインを取得するためのクラスであれば「GetHomeTimeLineAsyncTask」と命名して保存。

    1GetHomeTimeLineAsyncTask.kt
    2class GetHomeTimeLineAsyncTask(var twitter: Twitter, var paging: Paging, refreshCallback: RefreshCallback) : AsyncTask<Void, List<Status>?, List<Status>?>() {
    3
    4    private val refreshCallbackReference: WeakReference<RefreshCallback> = WeakReference(refreshCallback)
    5
    6    override fun doInBackground(vararg params: Void?): List<twitter4j.Status>? {
    7        try {
    8            val timeline = twitter.getHomeTimeline(paging)
    9            // publishProgressを呼ばないとonProgressUpdateは呼ばれないみたい
    10            publishProgress(timeline)
    11            return timeline
    12        } catch (e: TwitterException) {
    13            e.printStackTrace()
    14        }
    15        return listOf()
    16    }
    17}

    AsyncTaskの継承は、doInBackgroundメソッドさえ継承していればOKです。

    ポイントとなるのは

    1private val refreshCallbackReference: WeakReference<RefreshCallback> = WeakReference(refreshCallback)

    RefreshCallBack、これはインターフェイス(interface)として作ります。

    このJavaが、最初から持つWeakReferenceが重要です。

    1RefreshCallBack.kt
    2interface RefreshCallback {
    3    fun addListItem(statusList : List<twitter4j.Status>?)
    4    fun refreshCompleted()
    5    fun progressUpdate(progress: List<twitter4j.Status>?)
    6}

    twitter4jを利用しているので、twitter4jをデータモデルと捉えてください。

    このRefreshCallBackを利用してFragmentへ参照を返します。

    1TimeLineFragment.kt
    2class TimeLineFragment() : Fragment(),
    3        SwipeRefreshLayout.OnRefreshListener,
    4        RefreshCallback { // RefreshCallBackを実装
    5
    6        // 略
    7
    8    /*
    9     * @return List<Status>?
    10     * タイムラインを取得し、callbackに返す
    11     */
    12    fun reloadTimeLine() {
    13        val paging = Paging()
    14        paging.count = 200
    15        GetHomeTimeLineAsyncTask(twitter, paging, this).execute()
    16    }
    17
    18
    19}

    あとは、呼び出し元となるTimeLineFragmentにてRefreshCallbackインターフェイスの下記3メソッドを経由して、UIの更新処理などを行います。

    1/*
    2 * RefreshCallBack#addListItem
    3 * GetHomeTimeLineAsyncTaskのonPostExecuteがなされた時に呼ばれる
    4 */
    5override fun addListItem(statusList: List<Status>?) {
    6    timeLine = statusList
    7    timeLine?.let {
    8        if (it.isEmpty()) {
    9            Snackbar.make(view!!, getString(R.string.failed_to_load_timeline), Snackbar.LENGTH_SHORT).show()
    10        }
    11    }
    12}
    13
    14/**
    15 * RefreshCallBack#refreshCompleted
    16 * 非同期タスクが終了したらする動き
    17 * GetHomeTimeLineAsyncTaskのonPostExecuteがなされた時に呼ばれる
    18 */
    19override fun refreshCompleted() {
    20    recyclerTweetViewAdapter = TweetAdapter(activity!!.applicationContext, timeLine)
    21    recyclerView.apply {
    22        adapter = recyclerTweetViewAdapter
    23        adapter?.notifyDataSetChanged()
    24        mProgressBar.visibility = ProgressBar.INVISIBLE
    25    }
    26    mSwipeRefreshLayout.isRefreshing = false
    27}
    28
    29/**
    30 * RefreshCallBack#progressUpdate
    31 * 非同期タスクが進行中の時
    32 * GetHomeTimeLineAsyncTask#onProgressUpdate(vararg values: Int?)
    33 */
    34override fun progressUpdate(progress: List<Status>?) {
    35    progress?.forEach {
    36        mProgressBar.progress++
    37    }
    38}
    39
    40/**
    41 * swipeRefresh
    42 **/
    43override fun onRefresh() {
    44    mProgressBar.visibility = ProgressBar.VISIBLE
    45    reloadTimeLine()
    46}

    onRefreshメソッドに関してはSwipeRefreshを使う上で実装しています。

    WeakReferenceを経由して参照を渡すことで、非同期タスクがUIリソースの解放を遮らないようにします。

    最終的なAsyncTask

    最終的にAsyncTaskは、以下のようになります

    1class GetHomeTimeLineAsyncTask(var twitter: Twitter, var paging: Paging, refreshCallback: RefreshCallback) : AsyncTask<Void, List<Status>?, List<Status>?>() {
    2
    3    private val refreshCallbackReference: WeakReference<RefreshCallback> = WeakReference(refreshCallback)
    4
    5    override fun doInBackground(vararg params: Void?): List<twitter4j.Status>? {
    6        try {
    7            val timeline = twitter.getHomeTimeline(paging)
    8            // publishProgressを呼ばないとonProgressUpdateは呼ばれない
    9            publishProgress(timeline)
    10            return timeline
    11        } catch (e: TwitterException) {
    12            e.printStackTrace()
    13        }
    14        return listOf()
    15    }
    16
    17    override fun onPostExecute(result: List<twitter4j.Status>?) {
    18        super.onPostExecute(result)
    19        val callBack = this.refreshCallbackReference.get()
    20        callBack?.let {
    21            it.addListItem(result)
    22            it.refreshCompleted()
    23        }
    24    }
    25
    26    override fun onProgressUpdate(vararg values: List<twitter4j.Status>?) {
    27        val callback = this.refreshCallbackReference.get()
    28        callback?.let {
    29
    30            values[0]?.let { it1 -> it.progressUpdate(it1) }
    31        }
    32    }
    33}

    AsyncTaskでよく使うメソッド

    個人的にAsyncTaskでよく使うメソッドは上記で書いた

    1. doInBackground(必須の継承メソッド)
    2. onPostExecute(オプショナルだがよく使うメソッド)
    3. onProgressUpdate(オプショナルだがよく使うメソッド2)

    となります。

    onProgressUpdateに関してはアプリのユーザーにデータの読み込み情報を表示するProgressBarなどの表示のためによく使います。

    また、onProgressUpdateメソッドは、doInBackgroundメソッドでpublishProgressメソッドを呼ぶ必要がある点も注意しましょう。

    AsyncTaskの使い方 まとめ

    1. AsyncTask を使うなら WeakReference と CallBackインターフェイスを介することでメモリリークを防ぐ
    2. AsyncTask#onProgressUpdate が動作するには AsyncTask#doInBackground で pubishProgress() を呼ぶ必要がある
    3. 呼び出し元の Fragment では RefreshCallBack を介して、UIの処理を行えば良い

    これらの対応により、メモリリークの危険性は低減され、見通しの良いコードが出来上がるはずです。

    1This AsyncTask class should be static or leaks might occur...

    筆者自身、よく見かけるAndroid Studioからの警告もこのようにして撲滅できました。

    古くから動くAndroidアプリの保守開発を行っている皆様、もしAsyncTaskが存在してAndroid Studioから警告されている場合は上記の対応で修正を試みてはいかがでしょうか?

    こちらの記事もオススメ!

    featureImg2020.08.14スマホ技術 特集Android開発Android開発をJavaからKotlinへ変えていくためのお勉強DelegatedPropert...

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background