Android 非同期処理後の画面更新のきほん

2012.03.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Androidは、コンポーネントの全ての処理を メインスレッド(UIスレッドとも呼ばれるそうです) で実行する様に作られているそうです。

時間のかかる処理を実行する場合は、 メインスレッドとは別に新しくスレッドを作り、そこで処理させる場合が多いと思うのですが、 メインスレッドとは別のスレッドでの処理が終わった後に 画面を更新しようとすると例外が発生してします。 画面の更新が行えるのはメインスレッドからのみだからだそうです。

■メインスレッド以外のスレッドから画面を更新させるには?

メインスレッド以外のスレッドから画面を更新しようとするには、 別スレッドから直接画面を更新するのではなく、メインスレッドに対して画面更新処理を メッセージとして送る様にします。 メインスレッドがメッセージ(処理)を受け取り、実行することで画面が更新されます。 そうすると例外は発生しません。これは、メッセージキューという仕組みになります。

■LooperとHandler

Androidには、メッセージキューの処理を行う為に2つのクラスが用意されています。 android.os.Handlerとandroid.os.Looperです。

android.os.Looperはメインスレッドを監視し、 自身が持っているメッセージキューからメッセージを取り出して処理を実行します。 メインスレッドにはメッセージキューが関連付けられています。 Looperがメッセージを処理するということは、メインスレッドで実行される という事になります。

android.os.Handlerはメッセージキューにメッセージを入れる為のものです。 処理をメッセージとして格納しておくと メインスレッドがメッセージを受け取ったタイミングで格納した処理を実行してくれます。

■例外が発生してしまう例

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"
    	android:text="非同期処理開始"
    	android:id="@+id/button"
    />

	<TextView
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"
		android:id="@+id/textView"
	/>
</LinearLayout>

ThreadTestActivity.java

package com.androidtest.sample;

import android.app.Activity;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class ThreadTestActivity extends Activity {

	private ProgressDialog progressDialog;
	private TextView textView;
	private Button button;

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

        //main.xmlに設定したコンポーネントをid指定で取得します。
        button      = (Button)findViewById(R.id.button);
        textView    = (TextView)findViewById(R.id.textView);

        //ProgressDialogを生成します。
        progressDialog = new ProgressDialog(this);
        progressDialog.setMessage("実行中です。");

        //buttonがクリックされた時の処理を登録します。
        button.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				buttonProcess();
			}
		});
    }

    /**
     * buttonがクリックされた時の処理
     */
    private void buttonProcess() {
    	//ProgressDialogを表示します。
    	progressDialog.show();

    	//スレッドを生成して起動します。
    	MyThread thread = new MyThread();
    	thread.start();
    }

    class MyThread extends Thread {
        public void run() {
        	//時間のかかる処理実行します。今回は仮で10秒停止させています。
            try {
                //10秒停止します。
                Thread.sleep(10000);
            } catch (InterruptedException e) {
            }
            //画面のtextViewへ"処理が完了しました。"を表示させる。
            textView.setText("処理が完了しました。");

            //ProgressDialogを消去します。
            progressDialog.dismiss();
        }
    }
}

buttonがクリックされた時の処理の中で メインスレッドとは別のスレッドを開始しています。 メインスレッドとは別のスレッドMyThreadのrun()の中で、 時間のかかる処理を行っています(今回は時間のかかる処理のかわりとして10秒停止させています) その後で、textView.setText("処理が完了しました。"); と画面の更新処理を直接実行しようとする為、 CalledFromWrongThreadExceptionという例外が発生します。

■例外が発生せずに画面更新される例

main.xmlは■例外が発生してしまう例同じです。

package com.androidtest.sample;

import android.app.Activity;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class ThreadTestActivity extends Activity {

	private ProgressDialog progressDialog;
	private TextView textView;
	private Button button;
	Handler mHandler;

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

        //ハンドラを生成
        mHandler = new Handler();

        //main.xmlに設定したコンポーネントをid指定で取得します。
        button      = (Button)findViewById(R.id.button);
        textView    = (TextView)findViewById(R.id.textView);

        //ProgressDialogを生成します。
        progressDialog = new ProgressDialog(this);
        progressDialog.setMessage("実行中です。");

        //buttonがクリックされた時の処理を登録します。
        button.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				buttonProcess();
			}
		});
    }

    /**
     * buttonがクリックされた時の処理
     */
    private void buttonProcess() {
    	//ProgressDialogを表示します。
    	progressDialog.show();

    	//スレッドを生成して起動します。
    	MyThread thread = new MyThread();
    	thread.start();
    }

    class MyThread extends Thread {
        public void run() {
        	//時間のかかる処理実行します。今回は仮で10秒停止させています。
            try {
                //10秒停止します。
                Thread.sleep(10000);
            } catch (InterruptedException e) {
            }

            //メインスレッドのメッセージキューにメッセージを登録します。
            mHandler.post(new Runnable() {
            	//run()の中の処理はメインスレッドで動作されます。
                public void run() {
                    //画面のtextViewへ"処理が完了しました。"を表示させる。
                    textView.setText("処理が完了しました。");

                    //ProgressDialogを消去します。
                    progressDialog.dismiss();
                }
            });
        }
    }
}

mHandler.post(new Runnable() {…} でメインスレッドのメッセージキューにメッセージ(画面の更新処理) を登録します。 そうすると、メインスレッドでよいタイミングの時に 登録したメッセージ(処理)を受け取り、 実行してくれるので例外は発生しません。

■まとめ

  • 画面の更新処理を行えるのはメインスレッド(UIスレッド)のみ
  • 別スレッドから、画面更新処理を実行しようとすると例外(CalledFromWrongThreadException)が発生する
  • 別スレッドから、画面更新処理をするときはメッセージキューのしくみをつかう

非同期処理を実行する為のメッセージキューの仕組みの他に AsyncTaskクラスを使って非同期処理を実行する方法があるそうなので、 次回はそちらを試してみます。