Skip to content

Commit 45f23ea

Browse files
kiryldzjush
andauthored
Use unified initializer to workaround crash on startup (#2116)
* wip * wip * wip * Finalize MapboxInitializer * update docs * Rs/extend kdz unified initializer (#2122) * Avoid repeating code and simplify init logic for optional SDKs * Do not store AppInitializer in static block --------- Co-authored-by: Ramon <[email protected]> * Fixes / improvements * Clearer delta * Startup dependency * PR fixes * Private api file * Gather some info when crashing (#2123) * PR fixes * minor * Move exception out of companion * Descope Nav and Search * Manifest * Log time since initializer was called * Fix issue where initializer exception was overwritten * Do not reschedule on failure * Remove code related to Nav and Search and clean up docs * Upgraded metalava.txt * Ktlint * Moved everything to `sdk-base` to be closer to the gl-native and common imports * Address PR comments * Make MapboxInitializerException internal * Added doc for companion object for Dokka to pass. Make init JVM static * Fix explicit snapshot version, allow override (publish_android_snapshot) * Update the AWS CLI (publish_android_snapshot) * Downgrad startup lib to align with common * changelog * Downgrade startup lib --------- Co-authored-by: Ramon <[email protected]>
1 parent 77d0a09 commit 45f23ea

File tree

10 files changed

+205
-4
lines changed

10 files changed

+205
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ Mapbox welcomes participation and contributions from everyone.
44

55
# 10.16.3
66
## Bug fixes 🐞
7+
* Downgrade minimum required `compileSDK` from 31 to 30.
8+
* Fix the `java.lang.UnsatisfiedLinkError` exception happening on the startup.
79
* Fix widgets flickering due to race condition if they are animated.
810
* Fix widgets not showing on some zoom levels.
911
* Fix map being black when using widgets (e.g. when `MapDebugOptions.TILE_BORDERS` option is enabled).
10-
# 10.yy.zz
11-
## Features ✨ and improvements 🏁
12-
* Downgrade minimum required `compileSDK` from 31 to 30.
1312

1413
## Dependencies
1514
* Update Mapbox gestures library to 0.9.1
1615

16+
## Known issues
17+
* The `java.lang.UnsatisfiedLinkError` exception on startup has been fixed when using Mapbox Maps SDK __only__. If other Mapbox products are used (Navigation, Search) - loading navigation / search native libraries might still crash. Mapbox Navigation / Search SDKs fixes will be released separately.
18+
1719
# 10.16.2 November 08, 2023
1820
## Bug fixes 🐞
1921
* Fix a crash because of non-exported runtime-registered broadcasts receivers for apps targeting SDK 34.

buildSrc/src/main/kotlin/Project.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ object Dependencies {
5252
const val androidxRecyclerView = "androidx.recyclerview:recyclerview:${Versions.androidxRecyclerView}"
5353
const val androidxCoreKtx = "androidx.core:core-ktx:${Versions.androidxCore}"
5454
const val androidxAnnotations = "androidx.annotation:annotation:${Versions.androidxAnnotation}"
55+
const val androidxStartup = "androidx.startup:startup-runtime:${Versions.androidxStartup}"
5556
const val androidxInterpolators = "androidx.interpolator:interpolator:${Versions.androidxInterpolator}"
5657
const val androidxConstraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.androidxConstraintLayout}"
5758
const val androidxEspresso = "androidx.test.espresso:espresso-core:${Versions.androidxEspresso}"
@@ -119,6 +120,7 @@ object Versions {
119120
const val androidxCore = "1.6.0" // Latest version that supports compile SDK 30
120121
const val androidxFragmentTesting = "1.3.6" // Latest version that supports compile SDK 30
121122
const val androidxAnnotation = "1.1.0"
123+
const val androidxStartup = "1.1.0"
122124
const val androidxAppcompat = "1.3.0"
123125
const val androidxTest = "1.4.0"
124126
const val androidxArchCoreTest = "2.1.0"

gradle/sdk-registry.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ afterEvaluate {
1717
dryRun = false
1818
publish = true
1919
snapshot = isSnapshot
20+
override = isSnapshot
2021
publishMessage = "cc @mapbox/maps-android"
2122
publications = [currentComponent.name]
2223
excludeFromRootProject = project.ext.mapboxRegistryExcludeFromRootProject

sdk-base/api/PublicRelease/metalava.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ package com.mapbox.maps {
2222
@kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level, message="This API is experimental. It may be changed in the future without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface MapboxExperimental {
2323
}
2424

25+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class MapboxInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
26+
ctor public MapboxInitializer();
27+
method public Boolean create(android.content.Context context);
28+
method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
29+
method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=MapboxInitializerException::class) public static void init(android.content.Context context) throws java.lang.Throwable;
30+
field public static final com.mapbox.maps.MapboxInitializer.Companion Companion;
31+
}
32+
33+
public static final class MapboxInitializer.Companion {
34+
method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=MapboxInitializerException::class) public void init(android.content.Context context) throws java.lang.Throwable;
35+
}
36+
37+
public final class MapboxInitializerKt {
38+
}
39+
2540
public interface MapboxLifecycleObserver {
2641
method public void onDestroy();
2742
method public void onLowMemory();

sdk-base/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ dependencies {
5050
api(Dependencies.mapboxGlNative)
5151
api(Dependencies.mapboxCoreCommon)
5252
}
53+
implementation(Dependencies.androidxStartup)
5354

5455
testImplementation(Dependencies.junit)
5556
testImplementation(Dependencies.mockk)
Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,25 @@
1-
<manifest package="com.mapbox.maps.base"/>
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools"
3+
package="com.mapbox.maps.base">
4+
5+
<application>
6+
<provider
7+
android:name="androidx.startup.InitializationProvider"
8+
android:authorities="${applicationId}.androidx-startup"
9+
android:exported="false"
10+
tools:node="merge">
11+
<!-- Disable Common and Maps SDK Initializers -->
12+
<meta-data
13+
android:name="com.mapbox.common.MapboxSDKCommonInitializer"
14+
tools:node="remove" />
15+
<meta-data
16+
android:name="com.mapbox.maps.loader.MapboxMapsInitializer"
17+
tools:node="remove" />
18+
19+
<!-- Introduce the new unified initializer -->
20+
<meta-data
21+
android:name="com.mapbox.maps.MapboxInitializer"
22+
android:value="androidx.startup" />
23+
</provider>
24+
</application>
25+
</manifest>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.mapbox.maps
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import android.os.Looper
6+
import android.os.SystemClock
7+
import android.util.Log
8+
import androidx.annotation.MainThread
9+
import androidx.annotation.RestrictTo
10+
import androidx.startup.AppInitializer
11+
import androidx.startup.Initializer
12+
import com.mapbox.maps.loader.MapboxMapsInitializer
13+
import java.io.File
14+
15+
/**
16+
* Unified Mapbox SDKs initializer class that catches exceptions to avoid crashing during app
17+
* process start.
18+
*
19+
* Most of the crashes reported are related to [UnsatisfiedLinkError]
20+
* (https://github.com/mapbox/mapbox-maps-android/issues/1109).
21+
*
22+
* This solution is valid only when using Mapbox SDK for Android and no other Mapbox SDK (e.g.
23+
* Navigation, Search,...).
24+
*
25+
* In order to use this solution no other Mapbox SDK initializer should run (i.e.
26+
* [MapboxMapsInitializer] or [com.mapbox.common.MapboxSDKCommonInitializer]) during process start.
27+
* See the `sdk/src/main/AndroidManifest.xml` file.
28+
*/
29+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
30+
class MapboxInitializer : Initializer<Boolean> {
31+
32+
/**
33+
* This code is run exactly one time on process startup.
34+
*/
35+
override fun create(context: Context): Boolean {
36+
initializerCalledElapsedTime = SystemClock.elapsedRealtime()
37+
// try-catch to avoid terminating the whole process
38+
try {
39+
init(context)
40+
} catch (e: Throwable) {
41+
// Catch the exception, store it and log instead of propagating it. The app process will be
42+
// able to continue its start. The Mapbox SDK is not loaded and can't be used until `init`
43+
// function in the companion object is called. `MapView`, `MapSurface` and `Snapshotter` will
44+
// call the init by themselves.
45+
initializerFailure = e
46+
Log.w(TAG, "Exception occurred when initializing Mapbox: ${e.message}")
47+
}
48+
return true
49+
}
50+
51+
/**
52+
* We do not need any dependencies here.
53+
*/
54+
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
55+
return mutableListOf()
56+
}
57+
58+
/**
59+
* Companion object for [MapboxInitializer] that holds some static state to keep track of
60+
* initialization state and also provides [init] that does the SDK native stack initialization.
61+
*/
62+
companion object {
63+
private const val TAG = "MapboxInitializer"
64+
private var successfulInit = false
65+
private var currentAttempt = 0
66+
67+
/**
68+
* Elapsed time since boot when [MapboxInitializer.create] was called or null if it was not
69+
* called.
70+
*/
71+
internal var initializerCalledElapsedTime: Long? = null
72+
private set
73+
internal var initializerFailure: Throwable? = null
74+
private set
75+
76+
/**
77+
* This function initializes Maps SDK native stack if it has not yet been done successfully.
78+
*
79+
* It can be called multiple times. If the native stack was already initialized successfully
80+
* then this is a no-op.
81+
*
82+
* If initialization process throws an exception we catch it and enhanced it with system
83+
* information (see [MapboxInitializerException]).
84+
*/
85+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
86+
@MainThread
87+
@JvmStatic
88+
@Throws(MapboxInitializerException::class)
89+
public fun init(context: Context) {
90+
if (successfulInit) {
91+
return
92+
}
93+
if (Looper.myLooper() != Looper.getMainLooper()) {
94+
throw RuntimeException("Mapbox must be called from main thread only!")
95+
}
96+
// we operate with application context to avoid memory leaks with Activity / View context
97+
val applicationContext = context.applicationContext
98+
Log.i(TAG, "MapboxInitializer started initialization, attempt ${++currentAttempt}")
99+
runCatchingEnhanced(applicationContext) {
100+
// it is enough to call only MapboxMapsInitializer as it has dependency on MapboxSDKCommonInitializer
101+
AppInitializer.getInstance(applicationContext)
102+
.initializeComponent(MapboxMapsInitializer::class.java)
103+
Log.i(TAG, "MapboxInitializer initialized Maps successfully")
104+
}
105+
successfulInit = true
106+
}
107+
108+
/**
109+
* Runs the given [function]. If [function] throws a [Throwable] then a
110+
* [MapboxInitializerException] is thrown which contains extra information in it.
111+
*/
112+
@Throws(MapboxInitializerException::class)
113+
private inline fun runCatchingEnhanced(context: Context, function: () -> Unit) {
114+
try {
115+
function()
116+
} catch (t: Throwable) {
117+
// if we got to this point there we are most likely hitting UnsatisfiedLinkError, re-throw an exception
118+
throw MapboxInitializerException(currentAttempt, context, t)
119+
}
120+
}
121+
}
122+
}
123+
124+
internal class MapboxInitializerException(attempt: Int, context: Context, t: Throwable) :
125+
Throwable(gatherSystemInfo(attempt, context, t), t)
126+
127+
private fun gatherSystemInfo(attempt: Int, context: Context, t: Throwable): String {
128+
val isInstantApp = runCatching {
129+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
130+
context.packageManager?.isInstantApp
131+
} else {
132+
null
133+
}
134+
}
135+
val nativeLibs = runCatching {
136+
context.packageManager?.getApplicationInfo(context.packageName, 0)?.let { ai ->
137+
File(ai.nativeLibraryDir).list()?.joinToString() ?: ""
138+
}
139+
}
140+
val initializerCalledMsg = MapboxInitializer.initializerCalledElapsedTime?.let {
141+
// Log how long since the first time we tried to initialize during app process start. Or "null" if initializer was never called
142+
"initializer called ${SystemClock.elapsedRealtime() - it }ms ago"
143+
} ?: "initializer not called"
144+
145+
return "Failed to initialize: Attempt=$attempt," +
146+
" exception=[${t.javaClass.simpleName}]," +
147+
" $initializerCalledMsg," +
148+
" initializerFailure=[${MapboxInitializer.initializerFailure?.javaClass?.simpleName}]," +
149+
// Most likely initializerFailure is MapboxInitializerException so try to find its cause
150+
" initializerFailure.cause=[${MapboxInitializer.initializerFailure?.cause?.javaClass?.simpleName}]," +
151+
" extractedNativeLibs=[${nativeLibs.getOrNull()}]," +
152+
" isInstantApp=[${isInstantApp.getOrNull()}],"
153+
}

sdk/src/main/java/com/mapbox/maps/MapSurface.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class MapSurface : MapPluginProviderDelegate, MapControllable {
4646
surface: Surface,
4747
mapInitOptions: MapInitOptions = MapInitOptions(context) // could use strong ref here as MapInitOptions have strong ref in any case
4848
) {
49+
MapboxInitializer.init(context)
4950
this.context = context
5051
this.surface = surface
5152
this.mapInitOptions = mapInitOptions

sdk/src/main/java/com/mapbox/maps/MapView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ open class MapView : FrameLayout, MapPluginProviderDelegate, MapControllable {
9292
defStyleRes: Int,
9393
initOptions: MapInitOptions?,
9494
) : super(context, attrs, defStyleAttr, defStyleRes) {
95+
MapboxInitializer.init(context)
9596
val resolvedMapInitOptions = if (attrs != null) {
9697
parseTypedArray(context, attrs)
9798
} else {

sdk/src/main/java/com/mapbox/maps/Snapshotter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ open class Snapshotter {
4646
options: MapSnapshotOptions,
4747
overlayOptions: SnapshotOverlayOptions = SnapshotOverlayOptions()
4848
) {
49+
MapboxInitializer.init(context)
4950
this.context = WeakReference(context)
5051
mapSnapshotOptions = options
5152
snapshotOverlayOptions = overlayOptions

0 commit comments

Comments
 (0)