2011年11月14日月曜日

Activity#runOnUiThread(Runnable)の実装を読む&注意点

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
実はrunOnUiThreadを最近まで知らなくていつもアホみたいにHandler#postしていてなんだか悔しかったのでActivity#runOnUiThread(Runnable)の実装を読んでみました。

Activity#runOnUiThread


Activity#runOnUiThreadの実装がこちら。

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

内部的にHandler#postしていますね。
ここでちょっと気になるのが
Thread.currentThread() != mUiThread
この比較です。runOnUiThreadを実行しているスレッドがUIスレッドかどうかチェックしています。
そしてUIスレッドである場合は
action.run();
指定したRunnableをrunOnUiThreadの中で即実行しています。つまりrunOnUiThreadはRunnableを実行する為にブロッキングされるという事です。

どんな時問題になるか


runOnUiThreadをHandler#postと同じモノだと考えて利用しているとハマる可能性があります。
まぁ多分大丈夫と思いますが以下のhello2()様な実行順に依存性があるような実装をしちゃうとヤバイですね。

Handler mHandler = new Handler();
String mMessage = "hello!";
private void hello(){
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), mMessage, Toast.LENGTH_SHORT).show();
        }
    });
    mMessage = "good bye!";
}
private void hello2(){
    runOnUiThread(new Runnable(){
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), mMessage, Toast.LENGTH_SHORT).show();
        }
    });
    mMessage = "good bye!";
}

別スレッドからhello(),hello2()、UIスレッドからhello(),hello2()を実行するとToastに何が表示されるでしょうか?

呼び出しパターン 結果
別スレッドからHandler#postを呼ぶ(hello()) "good bye!"が表示される
UIスレッドからHandler#postを呼ぶ(hello()) "good bye!"が表示される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) "good bye!"が表示される
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) "hello!"が表示される

なんか一個動きが違うのがいるーーーーー。
はいそういう事ですね。


結論


色々省略しますが
・Handler#post, runOnUiThreadに渡すRunnableが触る変数にはHandler#post, runOnUiThread呼び出し後触らない。
・Handler#post, runOnUiThreadはなるべくメソッドの最後に呼ぶ
・Handler#post, runOnUiThreadに渡すRunnableの中では呼び出し元のメンバ変数などを触らない。
Handler#post, runOnUiThreadを呼び出すメソッド内でfinalな変数を宣言しておいてそれにアクセスしたりする様にする。

とかやってるといいんじゃないでしょかー


追記



といったリプライが。確かに。(@atsushienoさんありがとうございました!)

実際hello(), hello2()のmMessage = "good bye!";の直前にThread.sleep(1000);を追加して実行してみるとどうなるか実験してみました。
結果は以下です

呼び出しパターン 結果
別スレッドからHandler#postを呼ぶ(hello()) "hello!"が表示される
UIスレッドからHandler#postを呼ぶ(hello()) "hello!"が表示される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) "good bye!"が表示される
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) "hello!"が表示される

なんか一個動きが違うのがいるーーーーー。
はいそういう事ですね。
以下の様な事になります。

呼び出しパターン 動き
別スレッドからHandler#postを呼ぶ(hello()) 呼び出し後の処理とRunnableの実行どちらが先かは判らない
UIスレッドからHandler#postを呼ぶ(hello()) 必ず呼び出し後の処理が実行された後にRunnableが実行される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) 呼び出し後の処理とRunnableの実行どちらが先かは判らない
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) 必ずRunnableが実行された後に呼び出し後の処理が実行される


結論は変わらず。
どちらにせよHandler#post, runOnUiThreadを呼び出した後にHandler#post, runOnUiThread内部で利用されうる値などの書き換えはやばいよ、という事ですね。
多分誰もこういう事はしないとは思いますが・・・。多分。


0 件のコメント:

コメントを投稿