Skip to content

About

Minimal repro: Flutter HCPP (Android platform views v2) aborts the process when onEndFrame lands after the FlutterView detaches

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

HCPP onEndFrame crash repro

Minimal reproduction for a fatal abort in Hybrid Composition++ (HCPP / Android platform views v2):

[FATAL:flutter/shell/platform/android/platform_view_android_jni_impl.cc(2370)]
Check failed: fml::jni::CheckException(env).

The abort message does not carry the Java exception. It is:

java.lang.NullPointerException: Attempt to invoke interface method
  'boolean android.view.AttachedSurfaceControl.applyTransactionOnDraw(android.view.SurfaceControl$Transaction)'
  on a null object reference
	at io.flutter.plugin.platform.PlatformViewsController2.onEndFrame(PlatformViewsController2.java:676)
	at io.flutter.embedding.engine.FlutterJNI.endFrame2(FlutterJNI.java:1379)
	at android.os.MessageQueue.nativePollOnce(Native Method)
	...

Root cause

AndroidExternalViewEmbedder2::SubmitFlutterView posts onEndFrame2() from the raster thread to the platform thread on every frame, including frames with no platform layers (external_view_embedder_2.cc:73-91 and :168-203). By the time that task runs on the main looper, the FlutterView may have been detached from the controller, or detached from its window — and View.getRootSurfaceControl() is documented to return null when the view is not attached to a window. PlatformViewsController2.onEndFrame() guards neither:

// PlatformViewsController2.java:669
public void onEndFrame() {
  SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
  for (int i = 0; i < activeTransactions.size(); i++) {
    tx = tx.merge(activeTransactions.get(i));
  }
  activeTransactions.clear();
  flutterView.invalidate();
  flutterView.getRootSurfaceControl().applyTransactionOnDraw(tx);  // <-- NPE
}

Throwing here is fatal rather than merely wrong: the JNI caller ends with FML_CHECK(fml::jni::CheckException(env)), which converts any pending Java exception into abort(). There is no Dart-side recovery and nothing an app can do about it.

createOverlaySurface() (:716) dereferences getRootSurfaceControl() the same way, and FlutterJNI.endFrame2() throws RuntimeException when platformViewsController2 is null — two more paths to the same abort, not exercised by this repro.

What this app does

  • opts into HCPP via io.flutter.embedding.android.EnableHcpp (android/app/src/main/AndroidManifest.xml)
  • forces the Vulkan backend via io.flutter.embedding.android.ImpellerBackend — HCPP silently no-ops on the Impeller OpenGLES backend, so without this the repro renders nothing on an emulator
  • registers a trivial Kotlin PlatformView (a TextView) — no third-party plugins
  • creates it with PlatformViewsService.initHybridAndroidView, which always produces an HCPP view
  • caches the FlutterEngine in Application.onCreate so the engine outlives the activity

The cached engine is what makes this deterministic: it keeps FlutterJNI attached across an activity destroy, so detachFromView() runs while frames are still in flight. The same window exists during an ordinary activity teardown — detachFromFlutterEngine() runs before the engine is destroyed — which is presumably why this shows up in the field as a rare crash rather than an obvious one.

Requirements

  • Android 14+ (API 34) — HCPP is gated on SurfaceControl APIs
  • a Vulkan-capable device or emulator

Verified on an Android 16 (API 36) arm64 emulator, Flutter 3.44.4 / engine a10d8ac38de835021c8d2f920dbf50a920ccc030. Originally reported from the field on a Xiaomi HyperOS 3 (Android 16) device.

Steps

flutter run            # confirm the screen shows "HCPP supported: true" and a blue native view
./repro.sh             # launch/back loop

Or by hand: launch the app, wait for the blue native view, press back, launch again, press back.

Expected: no crash. Actual: the process aborts on every teardown where the platform view was on screen. Because the abort kills the process, the next launch is a cold start with no platform view yet, so a naive loop shows a crash on every second iteration — every warm teardown crashes.

Proposed fix

onEndFrame-fix.patch (and the full patched file, PlatformViewsController2.patched.java):

final AttachedSurfaceControl rootSurfaceControl =
    flutterView == null ? null : flutterView.getRootSurfaceControl();
if (rootSurfaceControl == null) {
  tx.close();
  return;
}

flutterView.invalidate();
rootSurfaceControl.applyTransactionOnDraw(tx);

tx.close() matters — the merged transaction owns native resources, so returning early without it leaks.

Verified

The patched class was compiled against the prebuilt flutter_embedding_debug jar and swapped in, so this is measured on-device, not just by inspection:

build teardowns aborts
stock engine 10 5 (every warm teardown)
patched engine 12 0

Platform view rendering and Flutter/platform view compositing are unaffected after re-attach.

Unrelated rough edge found along the way

A cached engine started headless (entrypoint executed in Application.onCreate, before any activity attaches) throws on the v2 channel:

MissingPluginException(No implementation found for method isSurfaceControlEnabled on channel flutter/platform_views_2)
MissingPluginException(No implementation found for method create on channel flutter/platform_views_2)

The flutter/platform_views_2 channel handler is only wired up once the engine attaches to an activity, so platform view creation silently fails on that first attach. This app works around it by rebuilding the platform view on AppLifecycleState.resumed.

About

Minimal repro: Flutter HCPP (Android platform views v2) aborts the process when onEndFrame lands after the FlutterView detaches

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages