Game Development Reference
In-Depth Information
Next comes the
FastRenderView.resume()
method. It is responsible for starting up the rendering
thread. Notice that we create a new
Thread
each time this method is called. This is in line with
what we discussed when we talked about the activity's
onResume()
and
onPause()
methods. We
also set the
running
flag to
true
. You'll see how that's used in the rendering thread in a bit. The
final piece to take away is that we set the
FastRenderView
instance itself as the
Runnable
of the
thread. This will execute the next method of the
FastRenderView
in that new thread.
The
FastRenderView.run()
method is the workhorse of our custom
View
class. Its body is
executed in the rendering thread. As you can see, it's merely composed of a loop that will stop
executing as soon as the
running
flag is set to
false
. When that happens, the thread will also be
stopped and will die. Inside the
while
loop, we first check to ensure that the
Surface
is valid. If it
is, we lock it, render to it, and unlock it again, as discussed earlier. In this example, we simply fill
the
Surface
with the color red.
The
FastRenderView.pause() method
looks a little strange. First we set the
running
flag to
false
.
If you look up a little, you will see that the
while
loop in the
FastRenderView.run()
method will
eventually terminate due to this, and hence stop the rendering thread. In the next couple of lines,
we simply wait for the thread to die completely, by invoking
Thread.join()
. This method will
wait for the thread to die, but might throw an
InterruptedException
before the thread actually
dies. Since we have to make absolutely sure that the thread is dead before we return from that
method, we perform the join in an endless loop until it is successful.
Let's come back to the
volatile
modifier of the
running
flag. Why do we need it? The reason
is delicate: the compiler might decide to reorder the statements in the
FastRenderView.pause()
method if it recognizes that there are no dependencies between the first line in that method and
the
while
block. It is allowed to do this if it thinks it will make the code execute faster. However,
we depend on the order of execution that we specified in that method. Imagine if the
running
flag were set after we tried to join the thread. We'd go into an endless loop, as the thread would
never terminate.
The
volatile
modifier prevents this from happening. Any statements where this member is
referenced will be executed in order. This saves us from a nasty Heisenberg—a bug that comes
and goes without the ability to be reproduced consistently.
There's one more thing that you might think will cause this code to explode. What if the surface
is destroyed between the calls to
SurfaceHolder.getSurface().isValid()
and
SurfaceHolder.
lock()
? Well, we are lucky—this can never happen. To understand why, we have to take a step
back and see how the life cycle of the
Surface
works.
We know that the
Surface
is created asynchronously. It is likely that our rendering thread will
execute before the
Surface
is valid. We safeguard against this by not locking the
Surface
unless
it is valid. That covers the surface creation case.
The reason the rendering thread code does not explode from the
Surface
being destroyed,
between the validity check and the locking, has to do with the point in time at which the
Surface
gets destroyed. The
Surface
is always destroyed after we return from the activity's
onPause()
method. Since we wait for the thread to die in that method via the call to
FastRenderView.pause()
,
the rendering thread will no longer be alive when the
Surface
is actually destroyed. Sexy, isn't it?
But it's also confusing.
We now perform our continuous rendering the correct way. We no longer hog the UI thread,
but instead use a separate rendering thread. We made it respect the activity life cycle as well,