Activity内に定義したAsyncTaskのネストクラスに警告「This 'AsyncTask' class should be static or leaks might occur」

この記事は、
「[非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照](https://qiita.com/wakwak/items/21da22045c2d084fdd33#comment-21887e3165e8d82845a9)」と題した投稿の続きです。
ですので、先にそっちをご覧いただければ幸いです。
そっちの記事は、頭ごなし的に言っちゃえば、「ネストクラスには、`static`修飾子を付けろ」です。

# いきなりカウントアップしだすアプリを作ってみた、ら...

起動したとたんに、勝手に1秒刻みでカウントアップが始まります。
START!から始まったら「9」まで数え上げたらお終いです。
（このアニメーションGIFは、3まで至ったらまたSTART!を繰り返しているアニメーションですが、このQiitaに貼り付けたアニメーションGIFを止めるすべがないのでごめんなさい）

![countup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100612/cd7b052b-f4ec-effc-27fc-531b50231003.gif)

環境は以下の通りです。

> Android Studio 3.6.1
> Build #AI-192.7142.36.36.6241897, built on February 27, 2020
> Runtime version: 1.8.0_212-release-1586-b04 amd64
> VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
> Windows 10 10.0
> GC: ParNew, ConcurrentMarkSweep
> Memory: 1237M
> Cores: 8
> Registry: ide.new.welcome.screen.force=true
> Non-Bundled Plugins:

Kotlinで開発しません。Javaでやります。

## Activityのコード上に警告が出ます

このアプリのActivityは以下の通りです。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {

    private TextView textView;

    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text_view);
        task = new MyTask();
        task.execute();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        task.cancel(true);
    }

    class MyTask extends AsyncTask<Void, String, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 10; i++) {
                if (isCancelled()) {
                    return null; // break;でもいいかも
                }
                try {
                    Thread.sleep(1000);
                    publishProgress(String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            textView.setText(values[0]);
        }
    }
}
```

そしたらAndroid Studioがこんな警告を出すんです。

![thisasynctaskclassshouldbestaticorleaksmightoccur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100612/69df9cc1-07ae-a500-a07b-3027117967d0.png)

> This 'AsyncTask' class should be static or leaks might occur

`MyTask`と名付けたネストクラスに、`static`修飾子が付いていないことを咎めています。

この警告を出さないようにする（した）のが、この記事の目的です。

# これがベストな（主流の）解決方法のようです

まず先に、結論のコードを掲載します。
エンクロージングクラスである`MainActicity`オブジェクトの**弱い参照**を、staticネストクラスのオブジェクトは保持することにします。ということで、`java.lang.ref.WeakReference`を使います。



```java:最善を尽くした修正
public class MainActivityGood extends AppCompatActivity {

    // TextViewのフィールドはやめます。

    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.text_view);
        // this渡してる？なんで？それはね、MyTaskにコンストラクタを設けたからさ。
        task = new MyTask(this);
        task.execute(textView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        task.cancel(true);
    }

    static class MyTask extends AsyncTask<Void, String, Void> {

        WeakReference<Activity> activityReference; // ここ注目！

        // コンストラクタを設けました。
        public MyTask(Activity activity) {
            // 弱い参照でエンクロージングクラスのオブジェクトを保持することにします。
            activityReference = new WeakReference<>(activity);
        }

        @Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 10; i++) {
                if (isCancelled()) {
                    return null; // break;でもいいかも
                }
                try {
                    Thread.sleep(1000);
                    publishProgress(String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            // エンクロージングクラスのオブジェクトの弱い参照は、getメソッドで取得できます。
            Activity activity = activityReference.get();
            // そして、その取得した弱い参照がnullになっちゃってないかのチェックはしておきましょう。
            if (activity == null || activity.isFinishing()) {
                return; // ※
            }

            TextView textView = activity.findViewById(R.id.text_view);
            textView.setText(values[0]);
        }
    }
}
```

結論はこうなんですが、せっかくなので途中私がこの結論に至るまでの逡巡を挟みつつ、最後には大団円を迎えたいと思います。

## ネストクラスにstatic修飾子を付ける

Android Studioの警告の仰せの通りに、盲目的に(笑)`MyTask`と名付けた`AsyncTask`サブクラスに`static`修飾子を付けました。

ただし、それだけの措置を施しただけですと、エンクロージングクラスの非staticフィールドをアクセスする際に以下の画像のように怒られます。なぜなのか。それは、 staticネストクラスは、エンクロージングのstaticメンバーにのみアクセス可能 だからです。

![Non-static field.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100612/7a49ba48-b3d2-efb7-2b7f-c6bcd36cfc6d.png)

だったら、

```java:MainActivityをこう修正すればイイじゃない？という安易な気持ちで
public class MainActivity extends AppCompatActivity {

    private static TextView textView;

    // 以下略
}
```

と修正するのは、否、それはチョット違うんでないかい？と気が咎めるのです。

## 我ながらひどいコードを書いたもんだ

わざとらしいのですが、まあ見てやってください。でも、こんなんで、Android Studioの例の警告は消えます。

```java:MainActivityDasai.java
public class MainActivityDasai extends AppCompatActivity {

    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.text_view);
        task = new MyTask();
        task.execute(textView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        task.cancel(true);
    }

    static class MyTask extends AsyncTask<TextView, Object, Void> {
        @Override
        protected Void doInBackground(TextView... textViews) {
            for (int i = 0; i < 100000; i++) {
                if (isCancelled()) {
                    return null; // break;でもいいかも
                }
                try {
                    Thread.sleep(1000);
                    publishProgress(textViews[0], String.valueOf(i));
                } catch (InterruptedException e) {
                    Log.e("MyTask", e.getMessage(), e);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Object... values) {
            ((TextView)values[0]).setText((String)values[1]);
        }
    }
}
```

私の逡巡が、我が身と心を迷走させております。

- この`MyTask`は、この`MainActivity`でしか使われないクラスだから、ネストクラスにしたい。
- しかしstaticネストクラスにせよとIDEが警告を出す。
- ところが`TextTview`型のフィールドはstaticフィールドにはしたくない。
- となると、この`MyTask`とこの`MainActivity`は、別クラスに分けるか。**でもそんなことをしたら、今はこの`TextTview`1個っきりだからまあいいけど、他にもいろんなViewを複数扱うということになると、参照の受け渡しが面倒だ。**
- そしたらやっぱりネストクラスにするか...。

この私の逡巡が、「 *では、`TextView`をエンクロージングクラスのフィールドにすること自体をやめよう！* 」という思いに至って、このようなダサいプログラムになりました。

而して、このダサいプログラムを書いた自分自身を咎めたくなりました。
どこが気にくわないかと言うと、**ジェネリクスに`Object`を指定する、というところに嫌気**がさします。
おかげで、「`((TextView)values[0]).setText((String)values[1]);`」の部分なんて、キャストしまくり、配列のインデックスどっちがどっち？と惑わせます。
加えて、`doInBackground`メソッドの引数そして`AsyncTask`の第1ジェネリクスを`TextView`にしたことも気にくわないです。`TextView`以外のViewが増えたらどうするんだよ、と。

## WeakReferenceを使って、もらったActivityを"弱い参照"で扱う

`java.lang.ref.WeakReference`を使って **弱い参照** として扱うことにしました。前掲のMainActivityGood.javaをご覧ください。

ポイントは、

- staticネストクラスにした`MyTask`にコンストラクタを定義する。
- エンクロージングクラスをジェネリックスで指定した`WeakReference`型フィールドも定義する。
- そしてそれをコンストラクタの引数を使って`new`する。これにて`MainActivityGood`の弱い参照を保持！
- あとは必要に応じて、`WeakReference`から`MainActivityGood`をゲット（メソッド名も`get()`）して使う。

前出のダサいやり方と違って、これなら`TextView`以外のViewを扱うことになっても対応が簡易になりますし、ジェネリクスに`Object`を指定するという愚なこともせずにすみます。

以上です。

# 参考

Qiitaのこの記事「[This Handler class should be static or leaks might occur](https://qiita.com/hideaki_sago/items/0e2308e55457d7c45ee3)」に近い話です。

# AsyncTaskはAPI レベル Rから非推奨になりました

`android.os.AsyncTask`は、API レベル R[^1]から非推奨になります。
この記事も、廃れる運命にあるわけです...。

[^1]: APIレベルは数字で示されるものなのに、この記事執筆時点では「R」となってます。

代替策は、

- java.util.concurrentパッケージをスタンダードに使うか、
- [Kotlin concurrency utilities](https://developer.android.com/topic/libraries/architecture/coroutines)を使ってね。

とのことです。後者は、要はKotlinのCoroutinesのことです。