マルチスレッドとUIの基本

ボタンのonClickイベント等で時間のかかる処理を行って、他のユーザーインターフェイス(以下UI)コンポーネントの処理が止まってしまったことはないだろうか。AndroidのUIはシングルスレッドで処理されるため、あるUIコンポーネントの処理が終わらないうちは次のUIコンポーネントの処理を行うことができないからだ。

ワーカースレッドの生成

Androidでは、UIを司るスレッドをメインスレッドあるいはUIスレッドと呼び、これをブロックさせてはならない。もし、ボタンのイベント等で時間のかかる処理を行いたい場合、ワーカースレッドを生成する等してメインスレッドをブロックしないようにすることが重要である。

        button1 = (Button)findViewById(R.id.button1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("onClick", "before Thread thread id = " + Thread.currentThread().getId());
                new Thread(new Runnable() {
                    public void run() {
                        Log.d("onClick", "in Thread button1 thread id = " + Thread.currentThread().getId());
                        try {
                            Thread.sleep(5000); // 5秒待つ
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        // 下のトーストで例外
                        Toast.makeText(getApplicationContext(), "Click button1", Toast.LENGTH_SHORT).show();
                    }
                }).start();
            }
        });

Threadのstartメソッドを呼び出すとすぐに戻り、runメソッドを実行する別のスレッドが生起される。Thread.currentThread.getIdメソッドを使えば、それぞれのスレッドのIDが異なることを確認できるだろう。

ところで、実は上記のコードはボタンを押下して5秒後にRuntimeExceptionがスローされてしまい正常に動作しない。原因は生成したスレッドからトーストを表示しようとしているからだ。先述のようにAndroidのUIはシングルスレッドで動作しており、メインスレッド以外からUIの操作を行ってはならない。このため、トーストを表示しようとしたところで例外がスローされてしまったのだ

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
    at android.os.Handler.(Handler.java:121)
    at android.widget.Toast.(Toast.java:68)
    at android.widget.Toast.makeText(Toast.java:231)

メインスレッド以外からのUI更新(Activity.runOnUiThread)

このような場合、ActivityのrunOnUiThreadメソッドを利用してトーストの表示をメインスレッドにポストする等すれば良い。runOnUiThreadメソッドはメインスレッドから呼ばれた場合、渡されたアクションをすぐに実行するが、メインスレッド以外から呼ばれた場合、渡されたアクションをメインスレッドのイベントキューにポストする。

runOnUiThreadメソッドを使って書き換えたコードは以下のようになる。

        button1 = (Button)findViewById(R.id.button1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("onClick", "before Thread thread id = " + Thread.currentThread().getId());
                new Thread(new Runnable() {
                    public void run() {
                        Log.d("onClick", "in Thread button1 thread id = " + Thread.currentThread().getId());
                        try {
                            Thread.sleep(5000); // 5秒待つ
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        runOnUiThread(new Runnable() {
                            public void run() {
                                Log.d("onClick", "runOnUiThread thread id = " + Thread.currentThread().getId());
                                Toast.makeText(getApplicationContext(), "Click button1", Toast.LENGTH_SHORT).show();
                            }
                        });
                    }
                }).start();
            }
        });

メインスレッド以外からのUI更新(View.post)

先ほどの例はワーカースレッドからトーストを表示する場合だったが、テキストビューを更新するような場合はViewのpostメソッドを利用することもできる。因みにTextViewはViewの直接のサブクラスである。

        button1 = (Button)findViewById(R.id.button1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("onClick", "before Thread thread id = " + Thread.currentThread().getId());
                new Thread(new Runnable() {
                    public void run() {
                        Log.d("onClick", "in Thread button1 thread id = " + Thread.currentThread().getId());
                        try {
                            Thread.sleep(5000); // 5秒待つ
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        textView1.post(new Runnable() {
                            public void run() {
                                Log.d("onClick", "post thread id = " + Thread.currentThread().getId());
                                textView1.setText("Click button1");
                            }
                        });
                    }
                }).start();
            }
        });

まとめ

Androidでマルチスレッドを利用する上で大事なことをまとめると以下である。

  • メインスレッド(UIスレッド)をブロックしない
  • メインスレッド(UIスレッド)以外からUIを更新しない

他にAndroid独自のAsyncTaskを利用する方法もある。

AsyncTaskによるスレッドの利用
(2012/02/14)

新着情報
【オープンソースソフトウェア環境構築】Apple silicon Macで開発環境を構築
【Rust Tips】Actix webでJSONをPOSTする
【Rust Tips】コマンドライン引数を取得する

Copyright(C) 2004-2013 モバイル開発系(K) All rights reserved.
[Home]