そんなスレッドで大丈夫か?

Androidアプリケーションでバックグラウンド処理をスマートに解決することができるAsyncTaskだが、気をつけることがある。

これはテスト用に書いたURLから画像をダウンロードして表示するアプリケーションだが、ボタンを押下されると画像をWWWから読込んでプログレスバーを更新する処理をUIスレッドを邪魔しないように、バックグラウンドで実行するためにAsyncTaskを使って書いている。

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    final Button btnGo = (Button)this.findViewById(R.id.btnDownload);
    final EditText edtUrl = (EditText)this.findViewById(R.id.edtURL);
    edtUrl.setText("http://f.hatena.ne.jp/images/fotolife/K/Kazzz/20101018/20101018174127.png");
    final ImageView image = (ImageView)this.findViewById(R.id.ImageView);
    final ProgressBar progress = (ProgressBar)this.findViewById(R.id.progressbar);
    
    btnGo.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            final AsyncTask at = new AsyncTask() {
                @Override
                protected Bitmap doInBackground(String... params) {
                    return getImage(edtUrl.getText().toString(), AsyncTest.this); //画像を取ってくる(省略)
                }
                @Override
                public void onProgressUpdate(Integer... values) {
                    for ( int p : values) {
                        progress.incrementProgressBy(p);
                    }
                }
                @Override  
                protected void onPostExecute(Bitmap result) {  
                    image.setImageBitmap(result);
                }  
            };
            at.execute(edtUrl.getText().toString());
        }
    });
}

このアプリケーションで実際にボタンを何度も押下してからDDMSパースペクティブのThreadビューを見ると、以下のようにAsyncTaskが生成したスレッドが複数見えるはずだ。


私は最初これを見た時に「これがAndroidアプリケーションで最も忌むべきコンテキストのリークか」※と思ったものだが、よくよく調べてみると今回の場合はそうではなかった。

上記でリークしているように見えるAsynTaskスレッドの正体は同クラスのソースコードの冒頭を見ればすぐに判る。

public abstract class AsyncTask {
    private static final String LOG_TAG = "AsyncTask";

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAXIMUM_POOL_SIZE = 128;
    private static final int KEEP_ALIVE = 10;

    private static final BlockingQueue sWorkQueue =
            new LinkedBlockingQueue(10);

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
:

本クラスでは内部で生成するスレッドの生成にThreadPoolExecutorを使用しており、これが同クラスのプールされているスレッドと処理すべきタスクのリストをスケジュールする訳だ。
第一パラメタCORE_POOL_SIZEは定数で5であり、スレッド数は最小5がキープされる。従って上のThreadビューで見えていた5つの残存スレッドは大丈夫だ、問題無い。(逆にスレッド上限は128となっている)

しかし、それにしても一旦成長したスレッドプールの最小プール数が5というのはいささか多い気がするのでオーバライドしたい所だが、ご覧の通りsThreadFactoryはスタティックであり、且つprivateであるためこのクラスを見本にして別なクラスを起こした方が良いだろう。


※Activity、Service等Contextの具象クラスの参照をクラスに持つことで発生する参照リークの事。Contextクラスにおけるインスタンスのライフサイクルは一般のJavaアプリケーションとは違うこともあり、最も起きやすい参照リークの一つである。
Avoiding Memory Leaks | Android Developers