When creating a SurfaceView it's normal to also create a separate thread to draw onto the surface. Is it better programming practice to have the thread be created and destroyed at the same time the activity is, or at the same time the surface is?
What are some of the advantages/pitfalls of either way?
The Activity
and the View
are created at essentially the same time. The Surface
is created later, and that's what the SufaceHolder callbacks are for.
You can't render on the Surface
before it exists or after it's destroyed, so there's no point in starting your rendering thread before then or leaving it running after. The tricky part is that the callbacks happen on the main UI thread (since that's where you set it up), so the surfaceDestroyed()
callback could be called while your render thread is doing work.
EDIT:
Some notes about the SurfaceView / Activity lifecycle are included below. These are now part of the official Android documentation; see Appendix B in the System-Level Graphics doc. The original post is available below for historical purposes.
You can see examples of both approaches in Grafika. Approach #1 (create/destroy thread in onResume/onPause) can be seen in TextureFromCameraActivity, approach #2 (create/destroy thread in surfaceCreated/surfaceDestroyed) can be seen in HardwareScalerActivity and RecordFBOActivity.
A few thoughts about app life cycle and
SurfaceView
.
There are two somewhat independent things going on:
- Application onCreate / onResume / onPause
- Surface created / changed / destroyed
When the Activity starts, you get callbacks in this order:
- onCreate
- onResume
- surfaceCreated
- surfaceChanged
If you hit "back", you get:
- onPause
- surfaceDestroyed (called just before the Surface goes away)
If you rotate the screen, the Activity
is torn down and recreated, so you get
the full cycle. (You can tell it's a "quick" restart by checking isFinishing()
.) It might be possible to start / stop an activity so quickly that surfaceCreated()
might happen after onPause()
, but I'm not sure about that.
If you tap the power button to blank the screen, however, you only get onPause()
--
no surfaceDestroyed()
. The Surface
remains alive, and rendering can continue (you
even keep getting Choreographer events if you continue to request them). If you have
a lock screen that forces a specific orientation your Activity
can get kicked, but
if not you can come out of screen-blank with the same Surface
you had before.
This raises a fundamental question when using a separate renderer thread with
SurfaceView
: should the lifespan of the thread be tied to the Surface
or to the
Activity
? The answer is: it depends on what you want to have happen when the screen
goes blank. There are two basic approaches: (1) start/stop the thread on Activity
start/stop; (2) start/stop the thread on Surface
create/destroy.
#1 interacts well with the app lifecycle. We start the renderer thread in onResume()
and
stop it in onPause()
. It gets a bit awkward when creating and configuring the thread
because sometimes the Surface will already exist and sometimes it won't. We can't simply
forward the Surface
callbacks to the thread, because they won't fire again if the
Surface
already exists. So we need to query or cache the Surface
state, and forward it
to the renderer thread. Note we have to be a little careful here passing objects between
threads -- best to pass the Surface
or SurfaceHolder
through a Handler
message, rather
than just stuffing it into the thread, to avoid issues on multi-core systems (cf.
Android SMP Primer).
#2 has a certain appeal because the Surface
and the renderer are logically intertwined.
We start the thread after the Surface
has been created, which avoids the inter-thread
communication concerns. Surface
created / changed messages are simply forwarded. We
need to make sure rendering stops when the screen goes blank, and resumes when it
un-blanks; this could be a simple matter of telling Choreographer to stop invoking the
frame draw callback. Our onResume()
will need to resume the callbacks if and only if
the renderer thread is running. It may not be so trivial though -- if we animate based
on elapsed time between frames, we could have a very large gap when the next event
arrives, so an explicit pause/resume message may be desirable.
The above is primarily concerned with how the renderer thread is configured and whether
it's executing. A related concern is extracting state from the thread when the
Activity
is killed (in onPause()
or onSaveInstanceState()
). Approach #1 will work
best for that, because once the renderer thread has been joined its state can be
accessed without synchronization primitives.