2012年1月6日

Android SDKのサンプル「LunarLander」でエラーが出るので修正してみた

Android SDKに含まれている「LunarLandar」というサンプルを試していて、エラーが出る事があるのに気付いた。

Homeキーなどで他のアプリケーションに移ってから LunarLander に戻って来た時に「Thread is already started」というエラーになる。

surfaceDestroyed で破棄されたThreadに対して、surfaceCreated で再び start() を呼んでしまっている事が原因らしい。

それにしても、標準のサンプルがどうしてこんな状態で放置されているんだろう。。。

ターゲットのVersionを1.5, 2.2, 4.03に変えて試してみたけれどもどれも同じ。
同じくSurfaceViewを使っている「JetBoy」というサンプルも試して見た。これも同じだった。

検索しても日本語の情報はあまり見つからなかった。
SurfaceViewとThreadのonPause、onResumeの処理について - shumach217の日記

英語だといくつかもう少し詳しい説明が見つかった。
Fixing the Lunar Lander example: a threaded gameloop for Android at michi's log 

Android - How to pause/unpause thread in games | Robert Green's DIY

Android crash when app is closed and reopened - Stack Overflow


対策方法としては以下の3つの選択肢が考えられそうだ。

  1. surfaceCreated で thread.start() する前にスレッドが無効な状態ではないか確認する。無効な場合はスレッドを新規作成(new)してから start する。
  2. surfaceDestroyed でスレッドを終了させずに、フラグを用いて擬似的に一時停止状態にする。
  3. surfaceDestroyed でスレッドを終了させずに、 object.wait() を用いてスレッドを一時停止状態にする。

まず1の方法でLunarLanderのソースを修正して試して見た。

これだとエラーは出なくなったけれども、復帰後の画面が真っ暗なまま何も表示されない。おそらく他の変数の内容が復元されていない状態になってしまうからだと思うが、きちんと動くようにしようとすると結構面倒そうなので、あきらめた。

次に2の方法。
変更点は以下の通り。
  • LunarThreadクラスに変数 mInBackground を追加。
  • isInBackground() and setInBackground() メソッドを追加。
  • run() メソッドで、isInBackground() をチェックして true なら 100ms スリープする様に変更。
  • surfaceDestroyed() メソッドでスレッドを停止しない様に変更。
  • surfaceCreated() メソッドで isInBackground() をチェックして true ならスレッドを start() するのではなく setInBackground(false) を呼ぶ様に変更。
変更後のLunarViewクラスのソースはこちら。
Fixed LunarView class in Android SDK Sample LunarLander. — Gist

これで一応問題なくバッチリ動く様になった。^^

ただ、100msのスリープを入れて擬似的にスレッドを一時停止状態にしているだけ、というのが、ちょっと気になる。

どうせならJavaの標準として定義されているwait/notifyの仕組みを使った方が良さそうな気がする。多分その方がCPUリソースの消費が少なくなりそうな気がする。多分。。。

ということで3の方法も試して見た。

変更点は、 run() メソッドでスレッドを待機させる時に sleep() ではなく wait() を使う様にした点だ。

変更後のLunarViewのソースはこちら。
Fixed LunarView class in Android sample LunarLander, using object.wait() method. — Gist


実際に2と3でCPUリソースの消費に違いがあるのか、Eclipseのデバッグモードでスレッドの状態を確認して見た。



2の方法の場合、Statusの表示が「timed_wait」になった後、何もしなくても utime, stimeの値が少しずつ増えていく。やはり少しずつCPUリソースを消費している様だ。

それに対して3の方法だと Statusが「wait」になり、utime, stime の値はピタっと止まったまま増えない。

端末のバッテリーにも多分この方が優しいだろうと思う。

ということで、スレッドを待機状態にする場合は、object.wait() と notify()/notifyAll() メソッドを使う方が良いみたいだ。