2015年12月25日
Androidのアプリ開発で、ゲームや動画などの描画などに使われている「SurfaceView」では、ビューが破棄されるタイミングと、描画用のスレッドを終了させるタイミングが調整されていないと、UIが操作不能となり、ANR(「応答していません。終了しますか?」の画面)が表示されてしまうことがあります。正しいタイミングでサーフェスを破棄することで、このようなリスクを解消できます。
SurfaceViewを使うと、UIを制御する通常のスレッド(「UIスレッド」とか「メインスレッド」と呼ばれる)とは別に、描画専用のスレッドを用意して、たっぷりと時間をかけて、コマ落ちしにくいゲームや動画を描画できます。
画面への描画は、基本的にUIスレッドでしか実行できませんが、SurfaceViewに限っては、描画に必要なCanvasを排他的に制御する仕組みを設けることで、別途用意した描画用のスレッドで描画処理を行うことができます。
描画用のスレッドで、ゲームの処理と描画を行えば、コマ落ちしにくいゲームを実現できます。
画面遷移などで別のアクティビティーを表示するときは、SurfaceViewの表示は終わります。
例えば、ゲームの画面で、「←」のナビゲーションボタンを押すと、画面はゲームを起動する前のアクティビティーに移ります。
ところで、ゲームを描画していたスレッドは、UIスレッドとは異なり、そのままではSurfaceViewの終了に気付けません。
SurfaceViewが描画できなくなるときは、UIスレッドで呼ばれるSurfaceHolder.CallbackのsurfaceDestroyed()で、描画を終了させる必要があります。
サーフェスの表示が終わり、SurfaceHolder.CallbackのsurfaceDestroyed()が戻ってしまった後は、もう、描画はできません。
描画スレッドからSurfaceHolderのlockCanvas()やunlockCanvasAndPost()の実行を試みると、例外が返ってきます。
lockCanvas()をサーフェス解放後に行うと、以下の例外が出ます。
12-25 17:10:21.487 19575-2047/jp.ne.sakura.sugi.test208
E/SurfaceHolder: Exception locking surface
java.lang.IllegalStateException: Surface has already been released.
at android.view.Surface.checkNotReleasedLocked(Surface.java:474)
at android.view.Surface.lockCanvas(Surface.java:256)
at android.view.SurfaceView$4.internalLockCanvas(SurfaceView.java:823)
at android.view.SurfaceView$4.lockCanvas(SurfaceView.java:791)
at jp.ne.sakura.sugi.test208.GameService.run(GameService.java:292)
at java.lang.Thread.run(Thread.java:818)
例外が出るのは、解放されたサーフェスに対しては、もうCanvasは取得できないからだと思われます。
unlockCanvasAndPost()をサーフェス解放後に行うと、以下の例外が出ます。
12-25 17:10:23.665 19575-2047/jp.ne.sakura.sugi.test208 W/System.err:
java.lang.IllegalStateException: Surface has already been released.
12-25 17:10:23.665 19575-2047/jp.ne.sakura.sugi.test208 W/System.err:
at android.view.Surface.checkNotReleasedLocked(Surface.java:474)
12-25 17:10:23.665 19575-2047/jp.ne.sakura.sugi.test208 W/System.err:
at android.view.Surface.unlockCanvasAndPost(Surface.java:277)
12-25 17:10:23.665 19575-2047/jp.ne.sakura.sugi.test208 W/System.err:
at android.view.SurfaceView$4.unlockCanvasAndPost(SurfaceView.java:861)
12-25 17:10:23.665 19575-2047/jp.ne.sakura.sugi.test208 W/System.err:
at jp.ne.sakura.sugi.test208.GameService.run(GameService.java:324)
12-25 17:10:23.665 19575-2047/jp.ne.sakura.sugi.test208 W/System.err:
at java.lang.Thread.run(Thread.java:818)
取得したCanvasを返す先がなくなってしまったので、例外が発生したと思われます。
単に例外が出るだけなら、適当にキャッチして、surfaceDestroyed()をきっかけに描画のループを抜けるようにフラグでも用意すれば問題は起こらないのですが、SurfaceViewが破棄されるタイミングによっては、以後表示されるアクティビティーが操作不能になってしまう問題があります。
具体的には、lockCanvas()を実行したときにはロックを取得できたけれども、unlockCanvasAndPost()を実行する前にサーフェスが解放されてしまうと、unlockCanvasAndPost()で例外が発生し、ロックを取得したまま放せなくなってしまうような感じになるので、操作不可能な状態に陥るなど、アプリの動作がおかしくなってしまいます。
SurfaceViewのサーフェスが解放されるタイミングは、ユーザのボタン操作をきっかけに決まるので、そのとき描画スレッドがCanvasをロックしていたかどうかで、アプリが以後操作不能になるというのは困ったことです。
とはいえ、解決策はちゃんとありました。
SurfaceHolder.Callbackのリファレンスマニュアルを読んでみたところ、対応方法がちゃんと書いてあります。
public abstract void surfaceDestroyed (SurfaceHolder holder)
This is called immediately before a surface is being destroyed. After returning from this call, you should no longer try to access this surface. If you have a rendering thread that directly accesses the surface, you must ensure that thread is no longer touching the Surface before returning from this function.
説明には「サーフェスがdestroyされる直前に呼ばれます。このコールから戻った後は、このサーフェスにアクセスすべきではありません。直接サーフェスにアクセスする描画スレッドがある場合は、この関数から戻る前に、確実に、描画スレッドがもうこのサーフェスを操作しないようにしてください。」と書いてあります。
UIスレッドから呼ばれるSurfaceHolder.CallbackのsurfaceDestroyed()は、サーフェスが解放されたら呼ばれるのではなく、サーフェスを解放される前に呼ばれています。サーフェスが解放されるのは、このメソッドが戻った後です。
従って、このメソッドの実行をしばらくブロックして、描画スレッドがCanvasを解放した後で、戻すようにすれば、描画スレッドがロックを保持したままになって、おかしな動作をしてしまうリスクをなくせます。
例えば、描画スレッドがもうサーフェスにアクセスしないことが確実になったら、描画スレッドからフラグをセットして、surfaceDestroyed()は、そのフラグがセットされたことを確認してから終わるようにすれば、アプリが操作不能になる現象は解消できます。
複数のスレッドを扱うと、いろいろ考えるべきことが出てきますが、処理能力の向上には代えがたい魅力があります。
杉原俊雄のホームページ→ Androidアプリ開発メモ(もくじ)
(c) 2015 Toshio Sugihara. All rights reserved.