バックグラウンドスレッドでダイアログを生成してはいけない

AndroidではUIスレッド上でGUI部品(ウィジェット)にアクセスするのが前提になっているため、他のスレッドでGUIにアクセスするとチェックが入り例外が発生する。

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

これを回避するためにはいろいろな方法があるが、一つはAsyncTaskの派生具象クラスを使ってスレッドでの処理とUIスレッドでのGUI処理を明確に分離することだ。

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());

メソッドdoInBackgroundはUIスレッドとは別なプールから取得されたスレッド上で実行されるのでGUI操作をしてはならない。代わりにUTスレッドと同期されるonProgressUpdateやonPostExecuteメソッド内でGUI操作を完了できる。

ならばと処理中にダイアログを表示してやろうと以下のようにコーディングすると

  • バックグラウンドスレッドでダイアログを生成する例
final AsyncTask at = new AsyncTask() {
    AlertDialog dialog;
    
    @Override
    protected Bitmap doInBackground(String... params) {
        this.dialog = new AlertDialog.Builder(AsyncTest.this)
          .setTitle("処理中")
          .setMessage(url + "を読込んでいます").create();
          
        return getImage(edtUrl.getText().toString(), AsyncTest.this); 
        //画像を取ってくる(詳細省略)
    }
    @Override
    public void onProgressUpdate(Integer... values) {
        if ( !dialog.isShowing() ) 
            this.dialog.show();
            
        for ( int p : values) {
            progress.incrementProgressBy(p);
        }
    }
    @Override  
    protected void onPostExecute(Bitmap result) {  
        image.setImageBitmap(result);
        this.dialog.dismiss();
    }  
};
at.execute(edtUrl.getText().toString());

すると今度は別な例外が発生する。

ERROR/AndroidRuntime(365): Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
ERROR/AndroidRuntime(365):     at android.os.Handler.(Handler.java:121)
ERROR/AndroidRuntime(365):     at android.app.Dialog.(Dialog.java:101)
ERROR/AndroidRuntime(365):     at android.app.AlertDialog.(AlertDialog.java:63)
ERROR/AndroidRuntime(365):     at android.app.AlertDialog.(AlertDialog.java:59)
ERROR/AndroidRuntime(365):     at android.app.AlertDialog$Builder.create(AlertDialog.java:786)
:

どうしてだ? showはUIスレッドで呼び出しているのだし生成は別スレッドで行ってもいいではないか。否。 Lopper.prepareの呼び出しが無いスレッド中つまりUIスレッド以外ではハンドラを生成できないのだ。

冒頭でUIスレッド以外のスレッドでGUI(ウィジェット)にアクセスすることができないと書いたが、ダイアログに関しては生成もUIスレッド以外では禁止されている。

ソースコードを追えばすぐに解ることだが、Handlerクラスのコンストラクタ内部でThreadLocalにLooperがセットされていない場合に例外となる。

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }

UIスレッドはActivityによって初期化され、LooperのThreadLocalへのセットもここで行われる。従ってUIスレッド以外のスレッドがLooperのインスタンスを保持していないのは当たり前なのである。(これに気がつくまでに時間かかったよなぁ)

この例外を避けるには

1. UIスレッドでダイアログを生成する
2. Looper.prepare()メソッドを予め実行する

この二通りが考えられるが、Looper.prepare()は本来Android Frameworkが呼ぶべきメソッドであり、原則としてUIスレッドでダイアログを生成すべきだろう。

  • UIスレッドでダイアログを生成する例
final AsyncTask at = new AsyncTask() {
    AlertDialog dialog;
    @Override
    protected Bitmap doInBackground(String... params) {
        return getImage(edtUrl.getText().toString(), AsyncTest.this); 
        //画像を取ってくる(詳細省略)
    }
    @Override
    public void onProgressUpdate(Integer... values) {
        if ( this.dialog == null ) {
	        this.dialog = new AlertDialog.Builder(AsyncTest.this)
	          .setTitle("処理中")
	          .setMessage(url + "を読込んでいます").create();
        }
          
        if ( !dialog.isShowing() ) 
            this.dialog.show();
            
        for ( int p : values) {
            progress.incrementProgressBy(p);
        }
    }
    @Override  
    protected void onPostExecute(Bitmap result) {  
        image.setImageBitmap(result);
        this.dialog.dismiss();
    }  
};
at.execute(edtUrl.getText().toString());

これは推測だが、この原則はダイアログだけではなくWindowを扱うToastでも同様だと考えられる。