2012年10月27日土曜日

GCMのIntentServiceでAsyncTaskを実行したらsending message to a Handler on a dead threadの警告が出て悩まされる

GCMサーバーからメッセージが送られてきた時の処理をGCMIntentServiceのonMessage()へ記述している。
my.app.GCMIntentService
  |- com.google.android.gcm.GCMBaseIntentService
      |- android.app.IntentService

onMessage()内でAsyncTaskクラスを作成してexecute()したところ、以下のようなWarningが出た。
10-26 15:24:14.025: W/MessageQueue(482): Handler{44f63ea8} sending message to a Handler on a dead thread
10-26 15:24:14.025: W/MessageQueue(482): java.lang.RuntimeException: Handler{44f63ea8} sending message to a Handler on a dead thread
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.MessageQueue.enqueueMessage(MessageQueue.java:179)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Handler.sendMessageAtTime(Handler.java:457)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Handler.sendMessageDelayed(Handler.java:430)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Handler.sendMessage(Handler.java:367)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.Message.sendToTarget(Message.java:348)
10-26 15:24:14.025: W/MessageQueue(482):  at android.os.AsyncTask$3.done(AsyncTask.java:214)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask$Sync.innerSet(FutureTask.java:252)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask.set(FutureTask.java:112)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:310)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.FutureTask.run(FutureTask.java:137)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
10-26 15:24:14.025: W/MessageQueue(482):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
10-26 15:24:14.025: W/MessageQueue(482):  at java.lang.Thread.run(Thread.java:1096)

コードのスニペット。
@Override
 protected void onMessage(final Context context, Intent intent) {
  Log.i(TAG, "Received message.");
  // IMAGE_IDを取得
  String imgId = intent.getStringExtra(IMAGE_ID);
  
  // Async version
   GetImgTaskTest getImgTaskTest = new
   GetImgTaskTest(context.getApplicationContext());
   getImgTaskTest.execute(imgId, null, null);
 }


StackOverFlowで、このWarningの回避策が触れられていた。

Issue 20915 - android - AsyncTask can get initialized with wrong Looper - Android - An Open Handset Alliance Project - Google Project Hosting

そこでは、onCreate()内でAsyncTaskクラスをロードしておくやり方が紹介されていた。
public class YourApplication extends Application {

    @Override
    public void onCreate() {

        // workaround for http://code.google.com/p/android/issues/detail?id=20915
        try {
            Class.forName("android.os.AsyncTask");
        } catch (ClassNotFoundException e) {
        }

        super.onCreate();
    }
}


これはAsyncTaskクラスのバグだという言及も。

android - onPostExecute not being called in AsyncTask (Handler runtime exception) - Stack Overflow

This is due to a bug in AsyncTask in the Android framework. AsyncTask.java has the following code:

private static final InternalHandler sHandler = new InternalHandler();

It expects this to be initialized on the main thread, but that is not guaranteed since it will be initialized on whichever thread happens to cause the class to run its static initializers. I reproduced this issue where the Handler references a worker thread.


しかし、このClass.forName()を使う方法は、あまりエレガントに思えなかったので、AsyncTaskのスレッドルールについて調べてみた。

AsyncTask | Android Developers "Threading rules" より
・The AsyncTask class must be loaded on the UI thread. This is done automatically as of JELLY_BEAN.
・execute(Params...) must be invoked on the UI thread.

AsyncTaskのAPIドキュメントにも、UI threadでクラスをロードせよ、execute()はUI threadで実行せよと、しっかり書いてある。


ナゼ?


まず基本的なことから。


・AndroidのUIは基本的にsingle threadモデルであることを理解すべし

AndroidのUIは基本的にsingle threadモデル。全てのアプリは1つのプロセスとスレッドで実行される。そのスレッドのことを別名"main" threadとよんでいる。

UI threadの中で、ネットワーク通信やデータベース検索など、時間がかかる処理を実行した場合、レスポンスが悪くなったり、アプリが止まっているように見えたりしてしまう。よって、以下の2点だけは守ろう。

Processes and Threads | Android Developers "Threads"より
Thus, there are simply two rules to Android's single thread model:

1. Do not block the UI thread
2. Do not access the Android UI toolkit from outside the UI thread


・時間がかかる処理はworker threadで行い、main threadから分けるべし

即座に応答しなくてよい操作だったら、それらは別のスレッド(バックグラウンドやworker threads)で処理できないか確認しよう。

Processes and Threads | Android Developers "Worker threads"より
If you have operations to perform that are not instantaneous, you should make sure to do them in separate threads ("background" or "worker" threads).

で、worker threadの結果は、以下の様なメソッドやクラスを使って反映させよう。



分かった!Warningの理由

そう、つまり、AsyncTaskは、worker threadがUI threadとコミュニケートしながら処理を進めるという操作を、簡単に書けるように作られたクラスなのだ。だから、execute()はUI thread内で実行することを想定している。

AsyncTaskのClass Overviewにも、真っ先に書いてある。

「AsyncTaskはUI threadを適切に、そして簡単に使えるようにしたもの。このクラスを使うと、ThreadやHandlerを使って自分で色々せずに、処理をバックグラウンドで行い、その結果をUI threadへ表示させることができる。」
AsyncTask enables proper and easy use of the UI thread. This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.

An asynchronous task is defined by a computation that runs on a background thread and whose result is published on the UI thread.


ここまで整理すると、Warningが出ていた理由も理解できる。

UI threadで実行するルールになっているAsyncTaskを、IntentService内で、つまりworker thread内でexecute()したていたからだ。

IntentSreviceではAsyncTaskは使えない。

非同期処理と思うとAsyncTaskと直ぐに思ってしまうけれど、常に使える手ではないということだろう。


じゃ、どうするか。


そもそもやりたかった処理は、メッセージを受信したら画像をダウンロードするというもの。その際Activityは起動しない、Notificationのみ表示する。onMessage()はその処理をキックするだけ。

ムム〜。やっぱりThraed回すのが手堅そう。となると、java.uti.concurrent.Executor あたりにヒントがありそう。