前人未踏の領域へ アプリ開発編

Android, iOSアプリ開発に関する調査メモ置き場。ほとんどAndroid。はてなダイアリーから移行したため古い記事にはアプリ以外も含まれます。

AsyncTaskをワーカースレッドから呼ぶべからず

Androidの古いバージョンでは以下の現象が発生しうるのでメモ。

現象

あるアプリでAsyncTaskのコンストラクタ呼び出し時に以下のようなエラーが発生。

java.lang.ExceptionInInitializerError
	at sample.service.servicetest.SampleService$1.run(SampleService.java:72)
	at java.util.Timer$TimerImpl.run(Timer.java:289)
Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
	at android.os.Handler.<init>(Handler.java:121)
	at android.os.AsyncTask$InternalHandler.<init>(AsyncTask.java:421)
	at android.os.AsyncTask$InternalHandler.<init>(AsyncTask.java:421)
	at android.os.AsyncTask.<clinit>(AsyncTask.java:152)
	... 2 more

再現方法

いくつかの条件がセットになると発生する。

  • UIスレッドとは別のワーカースレッドからAsyncTaskの呼び出しを行った。
  • アプリの起動後一番最初に実行したAsyncTaskがワーカースレッド側である
  • ワーカースレッドでAsyncTaskの呼び出し前にLooperの初期化を行っていない。

原因

  • AsyncTaskはstaticなフィールドにInternalHandlerというHandlerを持つ。
  • HandlerはLooperをインスタンスフィールドに持つ。
  • HandlerのコンストラクタではThreadLocalなLooperを取得しようとし、取得できないと例外を出力する。
  • AsyncTaskがUIスレッドで実行されていればmainLooperが返却されるのでHandlerはエラーにならない。
  • 別スレッドの場合はLooper.prepare()が実行されていないとLooper.myLooper()がnullを返す。

補足

  • Lopper.prepare()をワーカースレッド側で実行してAsyncTaskを実行すると、AsyncTask内では以後そのLooperが繰り返し使用され、onPostExecute()の結果もそのスレッドに返る。
  • onPreExecute()は呼び出しもとのスレッドから実行されるが、onPostExecuteはLooper内で処理されるためLooperの所属スレッドから実行される(未確認)。
  • 最新のコードではActivityThreadクラス内のmain()でAsyncTask.init()が実行されるのでstaticフィールドは初期化されている。必然的にAsyncTaskはUIスレッドのHandler(Looper)を持つため、onPostExecue()などのメソッドはUIスレッド側で実行される。