MapLibre Android Version
12.3.1 (still present on main @ 2a8ebc4)
Android SDK Version
Android 12 through 17 (API 31–37)
Device
Not device-specific — found by code inspection while investigating #4108. Reported from Topo Maps+ (co.topomaps) across 25 device models.
What happened?
MapRenderer::reset() destroys the Renderer but never clears rendererRef, so MapRenderer::actor() afterwards returns an ActorRef to a destroyed object.
void MapRenderer::reset() {
destroyed = true;
if (renderer) {
auto self = ActorRef<MapRenderer>(*this, mailboxData.getMailbox());
self.ask(&MapRenderer::resetRenderer).wait(); // renderer.reset()
}
std::scoped_lock lock(initialisationMutex);
rendererObserver.reset(); // rendererRef is never touched
}
ActorRef<Renderer> MapRenderer::actor() const { return *rendererRef; } // no guard
rendererRef is assigned only in MapRenderer::onSurfaceCreated, and reset() is reachable from Java at any point in a MapView's life: MapLibreSurfaceView.onDetachedFromWindow() calls nativeReset() unconditionally.
So after the MapView is detached from the window, every one of the twelve AndroidRendererFrontend entry points that calls actor() — setTileCacheEnabled, getTileCacheEnabled, reduceMemoryUse, queryRenderedFeatures (both overloads), querySourceFeatures, setFeatureState, getFeatureState, removeFeatureState, queryPointAnnotations, queryShapeAnnotations, queryFeatureExtensions — operates on a destroyed Renderer.
This is related to #4108 but distinct from it. #4108 is a null rendererRef before the GL thread ever constructs it; this is a dangling one after teardown. The queue-until-initialized approach in #4110 does not cover it, because from that PR's point of view the renderer was successfully initialized — nothing marks it as gone again.
NativeMapView.checkState() does not catch this either: its destroyed flag is set by MapView.onDestroy(), which a detach does not call.
Steps to reproduce
- Host a
MapView somewhere that detaches and re-attaches it rather than destroying it. Compose AndroidView reparenting a cached MapView is the common case — e.g. a screen transition that recomposes and calls parent.removeView(mapView) before returning the same instance.
- Once detached,
MapLibreSurfaceView.onDetachedFromWindow() → nativeReset() → MapRenderer::reset() destroys the Renderer.
- Before the new GL thread runs
onSurfaceCreated again, call anything that routes through AndroidRendererFrontend — map.tileCacheEnabled = false, mapView.onLowMemory(), or a queryRenderedFeatures from a queued tap.
I have not built a standalone reproducer; this was found by reading the source while tracking down #4108, and the window is timing-dependent in the same way. Treating it as a code-correctness report rather than a "here is an APK" report.
Renderer
OpenGL (choose this if you are unsure)
Relevant log output
No separate capture — our production crashes all group on the #4108 path
(null rendererRef at startup). This issue is the teardown-side sibling
found by inspection, filed separately so it does not get lost in that thread.
Additional context
A fix for #4108 that only guards the not-yet-initialized case will leave this one live. Both are covered by clearing rendererRef in reset() and having actor() — or its call sites in AndroidRendererFrontend — handle its absence, rather than every caller in the Java layer having to know the renderer's lifecycle.
Happy to test a patch against our production traffic; we currently see the #4108 variant at roughly 100 occurrences a month.
MapLibre Android Version
12.3.1 (still present on
main@ 2a8ebc4)Android SDK Version
Android 12 through 17 (API 31–37)
Device
Not device-specific — found by code inspection while investigating #4108. Reported from Topo Maps+ (co.topomaps) across 25 device models.
What happened?
MapRenderer::reset()destroys theRendererbut never clearsrendererRef, soMapRenderer::actor()afterwards returns anActorRefto a destroyed object.rendererRefis assigned only inMapRenderer::onSurfaceCreated, andreset()is reachable from Java at any point in a MapView's life:MapLibreSurfaceView.onDetachedFromWindow()callsnativeReset()unconditionally.So after the MapView is detached from the window, every one of the twelve
AndroidRendererFrontendentry points that callsactor()—setTileCacheEnabled,getTileCacheEnabled,reduceMemoryUse,queryRenderedFeatures(both overloads),querySourceFeatures,setFeatureState,getFeatureState,removeFeatureState,queryPointAnnotations,queryShapeAnnotations,queryFeatureExtensions— operates on a destroyedRenderer.This is related to #4108 but distinct from it. #4108 is a null
rendererRefbefore the GL thread ever constructs it; this is a dangling one after teardown. The queue-until-initialized approach in #4110 does not cover it, because from that PR's point of view the renderer was successfully initialized — nothing marks it as gone again.NativeMapView.checkState()does not catch this either: itsdestroyedflag is set byMapView.onDestroy(), which a detach does not call.Steps to reproduce
MapViewsomewhere that detaches and re-attaches it rather than destroying it. ComposeAndroidViewreparenting a cachedMapViewis the common case — e.g. a screen transition that recomposes and callsparent.removeView(mapView)before returning the same instance.MapLibreSurfaceView.onDetachedFromWindow()→nativeReset()→MapRenderer::reset()destroys theRenderer.onSurfaceCreatedagain, call anything that routes throughAndroidRendererFrontend—map.tileCacheEnabled = false,mapView.onLowMemory(), or aqueryRenderedFeaturesfrom a queued tap.I have not built a standalone reproducer; this was found by reading the source while tracking down #4108, and the window is timing-dependent in the same way. Treating it as a code-correctness report rather than a "here is an APK" report.
Renderer
OpenGL (choose this if you are unsure)
Relevant log output
Additional context
A fix for #4108 that only guards the not-yet-initialized case will leave this one live. Both are covered by clearing
rendererRefinreset()and havingactor()— or its call sites inAndroidRendererFrontend— handle its absence, rather than every caller in the Java layer having to know the renderer's lifecycle.Happy to test a patch against our production traffic; we currently see the #4108 variant at roughly 100 occurrences a month.