2013年4月10日水曜日

Activity#runOnUiThread(Runnable)のススメ

たとえば、非同期処理からUIスレッドに描画したくて、こんなコードを書いたとする。

private mHandler = new Handler();  private void sayHello() {      mHandler.post(new Runnable() {          @Override          public void run() {              Toast.makeText(this, "Hello?", Toast.LENGTH_SHORT).show();          }      }  };  

Handlerにpostしたメッセージをハンドルしないのならば、Activity#runOnUiThread(Runnable)に置き換えたほうがスッキリ。

private void sayHello() {      runOnUiThread(new Runnable() {          @Override          public void run() {              Toast.makeText(this, "Hello?", Toast.LENGTH_SHORT).show();          }      }  };  

説明

APIリファレンスにActivity#runOnUiThread(Runnable)の仕様が書かれている。

Runs the specified action on the UI thread. If the current thread is the UI thread, then the action is executed immediately. If the current thread is not the UI thread, the action is posted to the event queue of the UI thread.

UIスレッドから実行した場合と、他スレッドから実行した場合の振る舞いが異なる。

  • UIスレッドから実行した場合、引数のRunnableのrun()をすぐに実行
  • 他スレッドから実行した場合、引数のRunnableをHandlerにpostし、後でLooperがrun()を実行


(おそらく)この仕様は、UIスレッドからの処理と、他スレッドからの非同期処理を別々のメソッドとして用意しなくても良いように配慮されたものではないかと。

Handle#post(Runnable)を使う場合

たとえば、こんなコードを書いたとする。

// UIスレッドからの処理ではsayHello()を使うこと!  private void sayHello() {      Toast.makeText(this, "Hello?", Toast.LENGTH_SHORT).show();  };  // 他スレッドからの非同期処理ではsayHelloForAsync()を使うこと!  private void sayHelloForAsync() {      mHandler.post(new Runnable() {          @Override          public void run() {              sayHello();          }      }  };  

そして、誰かがこんなコードを書いたとする。

@Override  private void onClick(View v) {      sayHelloForAsync(); // [1]      Toast.makeText(this, "How low?", Toast.LENGTH_SHORT).show(); // [2]  };  

実行すると、[2] "How low?" → [1] "Hello?" の順にトーストが表示される。

Handlerにpostすると、RunnableがUIスレッドの後ろのほうにキューイングされるので、UIスレッドからsayHelloForAsync()をコールすると、コール元の処理をすべて完了([2] "How low?"を表示)してから、sayHelloForAsync()を実行([1] "Hello?"を表示)する。

ただし、このように動作するからといって、意図してこのようなコードを書くべきではない。

フィールド等を更新したり参照したりする処理が含まれていると不定な動作となるのでバグの温床となるし、そもそもUIスレッドからHandlerにpostした時の順序を保証する文書はどこにもない。

Activity#runOnUiThread(Runnable)を使う場合

Activity#runOnUiThread(Runnable)では「UIスレッドから実行した場合、引数のRunnableのrun()をすぐに実行」するため、別々のメソッドを用意する必要がない。

private void sayHello() {      runOnUiThread(new Runnable() {          @Override          public void run() {              sayHello();          }      }  };  

sayHello()を使って、誰かがこんなコードを書いたとする。

@Override  private void onClick(View v) {      sayHello(); // [1]      Toast.makeText(this, "How low?", Toast.LENGTH_SHORT).show(); // [2]  };  

実行すると、[1] "Hello?" → [2] "How low?" の順にトーストが表示される。

Runnableの生成や実行をするためのコストを気にする人がいるかも知れないが、UIスレッドには全体のパフォーマンスに影響するような処理をさせちゃダメだし、Runnableオブジェクトの生成を何回も繰り返しているのならば、それは単にお行儀の悪いJavaコードを書いたことが問題だと思う。

まとめ

Handlerをpostするメソッドは、非同期処理からコールされることを前提としているはずなので、単純にActivity#runOnUiThread(Runnable)へ置き換えても問題ない。

もし、Handlerを使ってUIスレッドの実行順序に依存するようなコードが書かれていたならば、置き換えはできないが、何らかの潜在バグがあると疑ったほうがいい。

0 件のコメント:

コメントを投稿