LoginSignup
1
1

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

Last updated at Posted at 2020-03-30

この記事は、
非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照」と題した投稿の続きです。
ですので、先にそっちをご覧いただければ幸いです。
そっちの記事は、頭ごなし的に言っちゃえば、「ネストクラスには、static修飾子を付けろ」です。

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

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

countup.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は以下の通りです。

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

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

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

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

これがベストな(主流の)解決方法のようです

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

最善を尽くした修正
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

だったら、

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

    private static TextView textView;

    // 以下略
}

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

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

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

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は、別クラスに分けるか。でもそんなことをしたら、今はこのTextTview1個っきりだからまあいいけど、他にもいろんな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」に近い話です。

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

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

代替策は、

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

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1