【Android】今更ながらAsyncTaskの警告に対応した話をまとめる【開発】

AndroidStudioメモリリークの警告Android
AndroidStudioメモリリークの警告

自作のAndroidアプリで非同期処理を行う部分のコードにて、Android Studioがずっと警告を出してきていました。

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

という警告を無視し続けてきました。この度対応することにしたので、androidにおけるAsyncTaskについて得た知見をメモしておきます。


よく使われる非同期処理クラス AsyncTask

AsyncTaskはAndroidのAPIレベル3から存在するもので、割とメジャーな非同期処理を行うためのクラスです。
今ではよくRxJava(RxAndroid)やLiveDataに置き換えられて、大規模なアプリではほとんど使っている事例を聞かないです。
しかしながら、私のような個人開発レベルのアプリですと、まだ使っている事例は多いのではないかと思います。

AsyncTaskのいいところ

  • 導入が楽。Rxなどのようにライブラリを追加することなく、AndroidSDKが標準で提供してくれている
  • 処理時間がそんなにかからない非同期処理などに使いやすい
  • 使い方さえ間違わなければ手軽

AsyncTaskの良くないところ

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

などなど、いろいろとよろしくないことが挙げられてしまう。

特にこれとか、記事自体がAndroid開発が流行りだした黎明期のものなので、今これを参照してプログラムを書くと漏れなくAndroid Studioから警告される。

ではどう実装するといいのか?

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

参考資料はこちら

Before 完全なるアンチパターン

AndroidStudioメモリリークの警告

AndroidStudioメモリリークの警告

こちらは完璧なるアンチパターン。漏れなく”This AsyncTask class should be static or leaks might occur…”とAndroid Studioから警告を受ける。画面が黄色くなる。

class MentionsFragment(val twitter: Twitter) : ListFragment(), SwipeRefreshLayout.OnRefreshListener {
    private var mAdapter: OldTweetAdapter? = null
    private var isSwipeRefresh = false
    private var mSwipeRefreshLayout: SwipeRefreshLayout? = null

... (略)

    /**
     * モールス信号を受信する
     */
    fun getMentionsTweet() {
        val paging = Paging()
        paging.count = GET_MENTION_TWEET_NUM

        val task = object : AsyncTask<Void, Void, List<Status>>() {
            override fun onPreExecute() {

            }

            override fun doInBackground(vararg params: Void): List<twitter4j.Status>? {
                try {
                    return twitter!!.getMentionsTimeline(paging)
                } catch (e: TwitterException) {
                    e.printStackTrace()
                }

                return null
            }

            override fun onPostExecute(result: List<twitter4j.Status>?) {
                if (result != null) {
                    mAdapter!!.clear()
                    for (status in result) {
                        mAdapter!!.add(status)
                    }
                } else {
                    showToast(getString(R.string.failed_to_load_timeline))
                }
                //くるくる消す
                mSwipeRefreshLayout!!.clearAnimation()
                mSwipeRefreshLayout!!.isRefreshing = false
            }
        }
        task.execute()
    }
}

自作アプリTwitMorseの処理の例に紹介する。getMorseTweetメソッドで匿名のAsyncTaskを生成し、その中で、mSwipeRefreshLayoutというSwipeRefreshLayout型のメンバ変数を操作している。SwipeRefreshLayoutはFragmentにつけられているUI部品なので、これはまさにアンチパターンなわけです。

After それなりに考慮したパターン

まず、AsyncTaskをディレクトリごと切ります。パッケージ右クリック→New→Package

そのパッケージ名を「asynctask」とします。そのディレクトリでNew→Kotlin File/Classを選択。
たとえばホーム・タイムラインを取得するためのクラスであれば「GetHomeTimeLineAsyncTask」と命名して保存。

class GetHomeTimeLineAsyncTask(var twitter: Twitter, var paging: Paging, refreshCallback: RefreshCallback) : AsyncTask<Void, List<Status>?, List<Status>?>() {

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

    override fun doInBackground(vararg params: Void?): List<twitter4j.Status>? {
        try {
            val timeline = twitter.getHomeTimeline(paging)
            // publishProgressを呼ばないとonProgressUpdateは呼ばれないみたい
            publishProgress(timeline)
            return timeline
        } catch (e: TwitterException) {
            e.printStackTrace()
        }
        return listOf()
    }
}

AsyncTaskの継承はdoInBackgroundさえ実装すればOKです。
ここでポイントとなるのは

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

RefreshCallBack、こいつはインターフェイス(interface)として作ります。
日本語でいうなれば、「RefreshCallbackのInterfaceを持ったWeakReference」というのでしょうか。ちょっと日本語に落とし込むのが面倒です。
とにかく、このJavaが最初から持つWeakReferenceが重要です。

package jp.sub.takelab.twitmorus.asynctask

interface RefreshCallback {
    fun addListItem(statusList : List<twitter4j.Status>?)
    fun refreshCompleted()
    fun progressUpdate(progress: List<twitter4j.Status>?)
}

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

ここまで来ると自分でも少々混乱しますが、そのままホームタイムライン用のFragmentで実装します。

open class BaseTimeLineFragment(val twitter: Twitter) : Fragment(),
        SwipeRefreshLayout.OnRefreshListener,
        RefreshCallback {

        // 略

}

※open は他クラスから継承されるためにつけています。まあ実は内部的には継承なんかしていないんですが。

この例として取り上げているBaseTimeLineFragmentには下記のようなメソッドを用意します。

    /**
     * @return List<Status>?
     * タイムラインを取得し、callbackに返す
     */
    fun reloadTimeLine() {
        val paging = Paging()
        paging.count = GET_TWEET_NUM
        GetHomeTimeLineAsyncTask(twitter, paging, this).execute()
    }

一応第3引数としてthis、つまりBaseTimeLineFragment自身のインスタンスを渡しています。こいつは

open class BaseTimeLineFragment(val twitter: Twitter) : Fragment(),
        SwipeRefreshLayout.OnRefreshListener,
        RefreshCallback {

としてBaseTimelineFragmentで実装しているRefreshCallbackを持つので、自分自身を渡せます。

あとは呼び出し元になるBaseTimeLineFragmentで以下を実装


/** * RefreshCallBack#addListItem * GetHomeTimeLineAsyncTaskのonPostExecuteがなされた時に呼ばれる */ override fun addListItem(statusList: List<Status>?) { timeLine = statusList timeLine?.let { if (it.isEmpty()) { Snackbar.make(view!!, getString(R.string.failed_to_load_timeline), Snackbar.LENGTH_SHORT).show() } } } /** * RefreshCallBack#refreshCompleted * 非同期タスクが終了したらする動き * GetHomeTimeLineAsyncTaskのonPostExecuteがなされた時に呼ばれる */ override fun refreshCompleted() { recyclerTweetViewAdapter = TweetAdapter(activity!!.applicationContext, timeLine) recyclerView.apply { adapter = recyclerTweetViewAdapter adapter?.notifyDataSetChanged() mProgressBar.visibility = ProgressBar.INVISIBLE } mSwipeRefreshLayout.isRefreshing = false } /** * RefreshCallBack#progressUpdate * 非同期タスクが進行中の時 * GetHomeTimeLineAsyncTask#onProgressUpdate(vararg values: Int?) */ override fun progressUpdate(progress: List<Status>?) { progress?.forEach { mProgressBar.progress++ } } /** * swipeRefresh **/ override fun onRefresh() { // 更新処理でもう一度TweetAdapterを更新するのはありなのか? mProgressBar.visibility = ProgressBar.VISIBLE reloadTimeLine() }

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

addListItem(statusList: List<Status>?)

refreshCompleted()

progressUpdate(progress: List<Status>?)

はRefreshCallBackにて宣言してあるメソッドになります。

こやつらをWeakReferenceを介してFragment側で実装するとキレイに警告が消えてくれます。

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


class GetHomeTimeLineAsyncTask(var twitter: Twitter, var paging: Paging, refreshCallback: RefreshCallback) : AsyncTask<Void, List<Status>?, List<Status>?>() { private val refreshCallbackReference: WeakReference<RefreshCallback> = WeakReference(refreshCallback) override fun doInBackground(vararg params: Void?): List<twitter4j.Status>? { try { val timeline = twitter.getHomeTimeline(paging) // publishProgressを呼ばないとonProgressUpdateは呼ばれないみたい publishProgress(timeline) return timeline } catch (e: TwitterException) { e.printStackTrace() } return listOf() } override fun onPostExecute(result: List<twitter4j.Status>?) { super.onPostExecute(result) val callBack = this.refreshCallbackReference.get() callBack?.let { it.addListItem(result) it.refreshCompleted() } } override fun onProgressUpdate(vararg values: List<twitter4j.Status>?) { val callback = this.refreshCallbackReference.get() callback?.let { values[0]?.let { it1 -> it.progressUpdate(it1) } } } }

BaseTimeLineFragment#addListItem(statusList: List?), refreshCompleted(),progressUpdate(progress: List?) とWeakReferenceを介してAsyncTaskの状態を取得することによって、Android Studioからの警告は消えます。


他tips

  • onProgressUpdateを呼ぶには、pubishProgress()メソッドを呼ばないといけない

ここで、ProgressBarなどのUI処理を行う。

AsyncTaskの使い方 まとめ

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

こんな感じで

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

を撲滅しました。

まあ、実はまだまだ、警告はあるのですが・・・。

参考記事

コメント

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