この記事は、
「非staticネストクラスが握っちゃう、エンクロージングオブジェクトの暗黙的参照」と題した投稿の続きです。
ですので、先にそっちをご覧いただければ幸いです。
そっちの記事は、頭ごなし的に言っちゃえば、「ネストクラスには、static
修飾子を付けろ」です。
いきなりカウントアップしだすアプリを作ってみた、ら...
起動したとたんに、勝手に1秒刻みでカウントアップが始まります。
START!から始まったら「9」まで数え上げたらお終いです。
(このアニメーションGIFは、3まで至ったらまたSTART!を繰り返しているアニメーションですが、このQiitaに貼り付けたアニメーション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は以下の通りです。
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がこんな警告を出すんです。
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メンバーにのみアクセス可能 だからです。
だったら、
public class MainActivity extends AppCompatActivity {
private static TextView textView;
// 以下略
}
と修正するのは、否、それはチョット違うんでないかい?と気が咎めるのです。
我ながらひどいコードを書いたもんだ
わざとらしいのですが、まあ見てやってください。でも、こんなんで、Android Studioの例の警告は消えます。
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」に近い話です。
AsyncTaskはAPI レベル Rから非推奨になりました
android.os.AsyncTask
は、API レベル R1から非推奨になります。
この記事も、廃れる運命にあるわけです...。
代替策は、
- java.util.concurrentパッケージをスタンダードに使うか、
- Kotlin concurrency utilitiesを使ってね。
とのことです。後者は、要はKotlinのCoroutinesのことです。
-
APIレベルは数字で示されるものなのに、この記事執筆時点では「R」となってます。 ↩