Skip to content

ImageReaderSurfaceProducer.onImageAvailable calls scheduleEngineFrame() without isAttached() guard, crashing after Activity stop when JNI detaches #188300

Closed

Description

@shivambhavsar

Describe the bug

ImageReaderSurfaceProducer.PerImageReader.onImageAvailableListener calls scheduleEngineFrame() (via onImage()) without first checking flutterJNI.isAttached(). The legacy SurfaceTextureRegistryEntry path guards this same call at line 345:

// SurfaceTextureRegistryEntry.onFrameListener (line 343–354) — has the guard:
if (released || !flutterJNI.isAttached()) {
    return;
}
scheduleEngineFrame();
// ImageReaderSurfaceProducer.PerImageReader.onImageAvailableListener (line 529–545) — missing the guard:
if (released || closed) {   // <-- no isAttached() check
    image.close();
    return;
}
onImage(r, image);          // → scheduleEngineFrame() at line 660

The result is a crash with "FlutterJNI is not attached to native" that is reproducible whenever:

  1. A SurfaceProducer-backed texture is actively receiving frames from a video decoder (e.g. ExoPlayer / Media3).
  2. The host Activity goes through onStop() → onDestroy().
  3. JNI detaches in onDestroy() before an async clearVideoSurface() posted to the ExoPlayer looper is processed — leaving a narrow window where in-flight onImageAvailable callbacks reach scheduleEngineFrame() on a detached JNI.

Steps to reproduce

  1. Create a Flutter Android plugin that uses TextureRegistry.createSurfaceProducer() to back a Texture widget.
  2. Bind an ExoPlayer (Media3) instance to producer.surface and start playback of a video.
  3. Navigate away from the Activity (Home button, or finish()) while the video is playing.
  4. The crash fires reliably under load (multiple concurrent players increases the hit rate); it is intermittent with a single player but consistent across sessions.

We observed 149 crash events across 142 users on a single app version before isolating the cause.

Expected results

scheduleEngineFrame() is not called after the JNI has detached, matching the behaviour of SurfaceTextureRegistryEntry.

Actual results

Fatal crash on the main thread:

java.lang.RuntimeException: FlutterJNI is not attached to native.
  at io.flutter.embedding.engine.FlutterJNI.ensureAttachedToNative(FlutterJNI.java)
  at io.flutter.embedding.engine.FlutterJNI.scheduleFrame(FlutterJNI.java)
  at io.flutter.embedding.engine.renderer.FlutterRenderer.scheduleEngineFrame(FlutterRenderer.java:1364)
  at io.flutter.embedding.engine.renderer.FlutterRenderer$ImageReaderSurfaceProducer.onImage(FlutterRenderer.java:660)
  at io.flutter.embedding.engine.renderer.FlutterRenderer$ImageReaderSurfaceProducer$PerImageReader.lambda$new$0(FlutterRenderer.java:544)

Flutter version

Flutter 3.44.0 • channel stable
Engine • revision 4c525dac5e
Dart 3.12.0

Proposed fix

The safest fix is to add the guard at scheduleEngineFrame() itself (line 1363), which protects all four call sites inside ImageReaderSurfaceProducer (lines 660, 730, 892, 1063) without requiring changes to any inner class:

// FlutterRenderer.java, line 1363
@VisibleForTesting
/* package */ void scheduleEngineFrame() {
+   if (!flutterJNI.isAttached()) return;
    flutterJNI.scheduleFrame();
}

flutterJNI is already a field of FlutterRenderer at this site. This mirrors the existing guard at line 436 in the same file (the texture-release Runnable).

Alternatively, a narrower fix adds the check inline in PerImageReader.onImageAvailableListener (line 540), mirroring the comment and pattern already present in SurfaceTextureRegistryEntry for the same race:

// FlutterRenderer.java, line 540
- if (released || closed) {
+ if (released || closed || !flutterJNI.isAttached()) {

flutterJNI is accessible here through the enclosing FlutterRenderer instance since ImageReaderSurfaceProducer is a non-static inner class.

Additional context

The SurfaceTexture path comment at line 339–342 explicitly acknowledges this class of race ("the callback may still be called by a stale reference after released==true and mNativeView==null") and defends against it with the isAttached() guard. The ImageReader path was introduced later and the same defence was not carried over.

The onImageAvailableListener is dispatched on new Handler(Looper.getMainLooper()) (line 547), so the race window is: Activity onStop() returns → Flutter JNI detaches in onDestroy() → a frame queued in the ImageReader before onStop fires its listener on the next main-looper tick → scheduleEngineFrame() on a detached JNI.

Plugin-level workarounds (calling producer.release() synchronously in onActivityStopped) do close the window via the existing released flag check but require recreating the producer and its texture id on every background/foreground cycle, which is unnecessary overhead. The engine-side fix eliminates the need for such workarounds entirely.

Activity

  1. added
    c: crashStack traces logged to the console
    platform-androidAndroid applications specifically
    engineflutter/engine related. See also e: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-androidOwned by Android platform team
    on Jun 22, 2026
  2. mbcorona commented on Jun 22, 2026

    @mbcorona
    Member

    Thanks for the detailed analysis and stack trace — this looks like the root cause of #174859, which has the same scheduleEngineFrame → ImageReaderSurfaceProducer.onImage crash chain but was stuck on needs-repro-info. Routing to team-android for investigatioon

  3. added
    P2Important issues not at the top of the work list
    triaged-androidTriaged by Android platform team
    on Jun 23, 2026
  4. shivambhavsar commented on Jul 7, 2026

    @shivambhavsar
    Author

    Following up, any update on the issue?

  5. bachras commented on Jul 31, 2026

    @bachras

    We are also seeing this crash in production with the identical stack:

    ImageReaderSurfaceProducer.onImage
    → FlutterRenderer.scheduleEngineFrame
    → FlutterJNI.scheduleFrame
    → FlutterJNI.ensureAttachedToNative
    

    Our environment is Flutter 3.44.0 (engine 4c525dac5e), camera: 0.12.0+1, camera_android_camerax: 0.7.1+2, and CameraX 1.5.3. We use a standard FlutterActivity with no cached engine, add-to-app integration, or custom platform views. Crashlytics shows active CameraX threads while the engine is detaching, consistent with an in-flight ImageReader callback arriving after JNI teardown. Our camera controllers already perform lifecycle cleanup, but asynchronous disposal cannot completely prevent this engine-level race.

    A guard in FlutterRenderer.scheduleEngineFrame() appears to be the safest fix because it protects every ImageReaderSurfaceProducer call site:

    if (!flutterJNI.isAttached()) {
      return;
    }
    flutterJNI.scheduleFrame();

    Please consider this another confirmed production occurrence. We can provide device and Android-version distribution details if useful.

  6. github-actions commented on Sep 16, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  7. locked as resolved and limited conversation to collaborators on Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listc: crashStack traces logged to the consoleengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions