libgdx: [Android] Loading of natives in static intializer makes it impossible to gracefully handle missing native libs

Issue details

Because GdxNativesLoader.load() is called in the static initializer of AndroidApplication, it’s not possible to run any code before it. This means that if native libs are not present the app immediately crashes, with no way to check for missing natives and handle the situation more gracefully (such as by displaying an error popup and closing the app).

This is an issue because of Google’s new app bundle functionality on Google Play. When app bundles are used, Google Play only distributes necessary resources to each device, including natives. This results in some nice space savings, but it also makes it very easy for someone to sideload the wrong APK and not get the natives they need.

I would think a better approach would be to load native libs in AndroidApplication.onCreate. This way code to check native libs could be run before calling super.onCreate. LibGDX could even perform the checking itself and display a standard error message, which a developer could then customize/override by manually checking earlier. This is much preferable to crashing as it won’t affect an application’s stability statistics on Google Play, and gives an opportunity to direct users to an APK which would work for them.

I’m not very familiar with the LibGDX codebase, so it’s possible I’m missing something here, but if there is agreement that moving native initialization to AndroidApplication.onCreate is a good idea I’d be happy to submit a pull request.

Reproduction steps/code

Can be reproduced in any libgdx android project by simply removing native libraries and commenting out native dependencies in build.gradle

Version of LibGDX and/or relevant dependencies

Tested on 1.9.10 but should affect all versions.

Stacktrace

(Example stack trace from a user crash on my game: Shattered Pixel Dungeon)

java.lang.ExceptionInInitializerError: 
  at java.lang.Class.newInstance (Class.java)
  at android.app.AppComponentFactory.instantiateActivity (AppComponentFactory.java:69)
  at androidx.core.app.CoreComponentFactory.instantiateActivity (CoreComponentFactory.java:43)
  at android.app.Instrumentation.newActivity (Instrumentation.java:1215)
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3008)
  at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3257)
  at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:78)
  at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:108)
  at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:68)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1948)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7050)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:965)
Caused by: com.badlogic.gdx.utils.GdxRuntimeException: 
  at com.badlogic.gdx.utils.SharedLibraryLoader.load (SharedLibraryLoader.java:125)
  at com.badlogic.gdx.utils.GdxNativesLoader.load (GdxNativesLoader.java:33)
  at com.badlogic.gdx.backends.android.AndroidApplication.<clinit> (AndroidApplication.java:60)
  at java.lang.Class.newInstance (Class.java)
  at android.app.AppComponentFactory.instantiateActivity (AppComponentFactory.java:69)
  at androidx.core.app.CoreComponentFactory.instantiateActivity (CoreComponentFactory.java:43)
  at android.app.Instrumentation.newActivity (Instrumentation.java:1215)
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3008)
  at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3257)
  at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:78)
  at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:108)
  at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:68)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1948)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7050)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:965)
Caused by: java.lang.UnsatisfiedLinkError: 
  at java.lang.Runtime.loadLibrary0 (Runtime.java:1012)
  at java.lang.System.loadLibrary (System.java:1669)
  at com.badlogic.gdx.utils.SharedLibraryLoader.load (SharedLibraryLoader.java:119)
  at com.badlogic.gdx.utils.GdxNativesLoader.load (GdxNativesLoader.java:33)
  at com.badlogic.gdx.backends.android.AndroidApplication.<clinit> (AndroidApplication.java:60)
  at java.lang.Class.newInstance (Class.java)
  at android.app.AppComponentFactory.instantiateActivity (AppComponentFactory.java:69)
  at androidx.core.app.CoreComponentFactory.instantiateActivity (CoreComponentFactory.java:43)
  at android.app.Instrumentation.newActivity (Instrumentation.java:1215)
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:3008)
  at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3257)
  at android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.java:78)
  at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:108)
  at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:68)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1948)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loop (Looper.java:214)
  at android.app.ActivityThread.main (ActivityThread.java:7050)
  at java.lang.reflect.Method.invoke (Method.java)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:965)

Please select the affected platforms

  • Android
  • iOS (robovm)
  • iOS (MOE)
  • HTML/GWT
  • Windows
  • Linux
  • MacOS

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 3
  • Comments: 18 (18 by maintainers)

Commits related to this issue

Most upvoted comments

  • We have a problem here
  • We have code that we know is not good here
  • We have no backwards compatiblity break for any games that doesn’t tweak special parts of the Android implementation, so I would state there is no breaking change

Hence I think we should make a change and not switch to “never change a running system” mode just because changes might introduce new problems. If we know the static initializers are a problem, that should be changed. If the change introduces new problems, then it should be changed again.

You can use Android UI classes (e.g. textview or alertdialog) to display errors in the libGDX activity just the same as a separate one. It’s probably a bad idea to expect the libGDX classes to gracefully handle cases where initialization fails either way though. Will just move loading to init with no other changes and submit a pull request soon.

I’ve ended up working around this a bit with a separate activity in my project: https://github.com/00-Evan/shattered-pixel-dungeon/blob/master/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidLauncher.java

Basically AndroidLauncher is now a vanilla android Activity which loads natives, launches AndroidGame (which extends AndroidApplication), and finishes. If an exception occurs it displays a text view with an error message instead.