Posted: 23 May 2013
After running into some weird bugs in my latest Libgdx game caused by misunderstanding the Libgdx lifecycle on Android, I spent some time trying to pin down exactly what can happen, why, and how to reliably reproduce different paths through the lifecycle graph.
I have not worked with the GWT, iOS or Desktop backends to see if/how they differ from the Android lifecycle.
Libgdx App Lifecycle on Android
There are a couple different ways the Android system can "exit" or "background" your libgdx application, and these have a different impact on the data in your application. As far as I can tell, there are three ways the application life-cycle can impact application state:
The Ideal Libgdx Application Lifecycle
First, the "ideal" application life-cycle from when the JVM starts up to when the JVM exits:
- Run static initializers
- Run Libgdx
create()
callback. - Run Libgdx
resize(int, int)
callback. - Repeatedly run Libgdx
render()
callback. - Run Libgdx
pause()
callback. - Run Libgdx
dispose()
callback. - JVM exits
Note that the "pause" callback is invoked when the app is exiting. Otherwise, this is pretty straight-forward and is just a point of comparison for the other two lifecycles. Also note that "resume" is never invoked in this lifecycle.
How to reproduce: make sure your app is not running in the background, then start it, and after its running for a bit, hit the "back" button to exit the application. You may have to wait a bit for the underlying JVM to exit.
The Pause/Resume Application Lifecycle
Next, the pause-resume lifecycle:
- Run static initializers
- Run Libgdx
create()
callback. - Run Libgdx
resize(int, int)
callback. - App runs and pauses (repeats this loop):
- Repeatedly run Libgdx
render()
callback. - Run Libgdx
pause()
callback. - Run Libgdx
resize(int, int)
callback. - Run Libgdx
resume()
callback.
- Repeatedly run Libgdx
- Run Libgdx
pause()
callback. - Run Libgdx
dispose()
callback. - JVM exits
This is the lifecycle that loses the OpenGL
context (between the
pause
and resume
). In general, Libgdx will
re-create OpenGL objects that were lost. But if, for example, you
have any run-time created textures, you will have to re-create them on
the resume path (or re-create them on demand if you're fancy).
References to OpenGL shader objects will also be lost.
Note that static
state and any create()
state survives across
OpenGL context loss.
How to reproduce: make sure your app is not running in the background, start your app, hit "HOME" to pause the application and quickly start the app again from the home screen (or from the "running apps" list).
The "JVM Recycle" Lifecycle
This is a more subtle lifecycle that runs a second application instance in an already-initialized JVM:
- Run static initializers
- Run Libgdx
create()
callback. - Run Libgdx
resize(int, int)
callback. - Repeatedly run Libgdx
render()
callback. - Run Libgdx
pause()
callback. - Run Libgdx
dispose()
callback. - Run Libgdx
create()
callback. - Run Libgdx
resize(int, int)
callback. - Repeatedly run Libgdx
render()
callback. - Run Libgdx
pause()
callback. - Run Libgdx
dispose()
callback. - JVM exits
This lifecycle goes through the complete libgdx application lifecycle
(create
to dispose
) but because the JVM is recycled, all the
static
state in the application is reused when the application is
started a second time.
How to reproduce: start your application, hit "BACK" to exit your application, then quickly start the application again. The original JVM should get reused for a "new" instance of your libgdx application.
Implications
There are several kinds of object/state references that you need to think about with respect to the application lifecycle:
static
state (or static initializers)create()
-time state- References to OpenGL state (e.g., texture, shader or VBO references).
Be careful with static
state. Any static object references may
survive from one application instance into another. Any references to
libgdx objects or to OpenGL objects will probably not be correct in
such cases. Its safest to avoid static state entirely, but you can
clean it up at dispose()
time, too.
Any background threads that you explicitly create are effectively
"static" state (unless you clean them up explicitly at dispose()
time). You cannot rely on the application exit triggering a JVM exit
to clean up your background threads.
Most OpenGL context is saved and restored by Libgdx. You only need to worry about OpenGL context in places where you use the OpenGL APIs directly. (And a couple places where the Libgdx API explicitly disclaims responsibility for the OpenGL state behind its object.) Generally, direct state is referenced if you generate textures at run-time or if you compile and link OpenGL shaders.
If you correctly implement your pause
, resume
, dispose
and
create
methods you should be able to handle all of these lifecycles
transparently to the user and consistently resume the application
where the user left off in any of the cases.
Quirks with the .resize()
callback
Because of the way that the resize
callback is implemented, it can
race with the resume
callback and may be delivered before or after.
Additionally, the resize
callback may be invoked redundantly or with
very short-lived sizes. For example, in my locked-landscape
application on my phone, after a resume from the lock screen (start
the app, hit the power button to lock the screen, then power again and
unlock to resume the app) I get a resize
callback for the "narrow,
wide" size (portrait -- which is what the lock screen shows), and then
the resume
callback runs, then I get a resize
callback for the
"wide, narrow" (landscape) size.
Rules
An ApplicationListener.create()
callback is always followed by a
.resize()
callback before the first .render()
. After initial
setup, however, the .resize()
callback is not so well behaved and
may be invoked multiple times in a row, and may be invoked before or
after pause/resume transitions. It may also be called redundantly in
some cases, so only do simple, idempotent work in the .resize()
callback.
An .pause()
callback will not be followed by a
.render()
callback without an intervening .resume()
callback.
Internally, the .resume()
callback will not be invoked until the
underlying Android onDrawFrame
method is invoked, so implicitly the
surface must be created before .resume()
can be invoked.
The .resume()
callback is only invoked after a .pause()
. This is
in contrast to the normal Android lifecycle where resume is also
invoked on the first start of the application. Thus in libgdx, a
.resume()
is always preceded by a .pause()
.
The .pause()
callback should be "quick" as it blocks the Android UI
from moving to the next activity until it is complete.
Background
Libgdx lifecycle events are built up from Android GLSurfaceView
and
Activity
lifecycle events. The Libgdx render thread is handling
the view's callbacks (onSurfaceCreated
, onSurfaceChanged
, and
onDrawFrame
) (see AndroidApplication.java
).
The Activity
lifecycle events (onResume
,
onPause
, etc) are forwarded to the render thread, and so are
synchronized with the view callback handlers (see AndroidGraphics.java
).