Skip to content

MapRenderer::reset() leaves rendererRef dangling; actor() returns a destroyed Renderer after MapView detach #4549

Description

@EllisGlaeser

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

  1. 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.
  2. Once detached, MapLibreSurfaceView.onDetachedFromWindow()nativeReset()MapRenderer::reset() destroys the Renderer.
  3. Before the new GL thread runs onSurfaceCreated again, call anything that routes through AndroidRendererFrontendmap.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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions