Posted:

Libgdx and Android application Lifecycle

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:

  1. Run static initializers
  2. Run Libgdx create() callback.
  3. Run Libgdx resize(int, int) callback.
  4. Repeatedly run Libgdx render() callback.
  5. Run Libgdx pause() callback.
  6. Run Libgdx dispose() callback.
  7. 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:

  1. Run static initializers
  2. Run Libgdx create() callback.
  3. Run Libgdx resize(int, int) callback.
  4. App runs and pauses (repeats this loop):
    1. Repeatedly run Libgdx render() callback.
    2. Run Libgdx pause() callback.
    3. Run Libgdx resize(int, int) callback.
    4. Run Libgdx resume() callback.
  5. Run Libgdx pause() callback.
  6. Run Libgdx dispose() callback.
  7. 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:

  1. Run static initializers
  2. Run Libgdx create() callback.
  3. Run Libgdx resize(int, int) callback.
  4. Repeatedly run Libgdx render() callback.
  5. Run Libgdx pause() callback.
  6. Run Libgdx dispose() callback.
  7. Run Libgdx create() callback.
  8. Run Libgdx resize(int, int) callback.
  9. Repeatedly run Libgdx render() callback.
  10. Run Libgdx pause() callback.
  11. Run Libgdx dispose() callback.
  12. 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:

  1. static state (or static initializers)
  2. create()-time state
  3. 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).

Comments or Questions?

Subscribe via RSS | Atom Feed