Mobile Development 27 min read

Resolving Thread Merging Issues in Flutter Multi‑Engine PlatformView Scenarios

This article explains the thread‑merging problem that arises when using PlatformView in Flutter’s multi‑engine setups, analyzes the root causes in both independent and lightweight engines, and presents a comprehensive solution that modifies the engine’s task‑runner merging logic to support one‑to‑many merges.

ByteDance Terminal Technology
ByteDance Terminal Technology
ByteDance Terminal Technology
Resolving Thread Merging Issues in Flutter Multi‑Engine PlatformView Scenarios

Abstract

This article introduces the unavoidable thread‑merging issue that occurs when using PlatformView in Flutter’s multi‑engine environment and describes the final solution that has been merged into the official Flutter engine repository.

Background

What is PlatformView? PlatformView is a mechanism that allows Flutter to embed native UI components (such as Android WebView, map controls, or third‑party SDK views) as Flutter widgets, providing a unified cross‑platform API.

Historically Flutter offered two implementations for PlatformView: the older VirtualDisplay method (pre‑1.20) and the newer HybridComposition method (recommended after 1.20). The article focuses on the thread model required for PlatformView.

Flutter Engine Thread Model

The engine defines four Task Runners that map to OS threads:

Platform Task Runner : Main UI thread handling user input and PlatformChannel messages.

UI Task Runner : Executes Dart code and builds the layer tree.

GPU (Raster) Task Runner : Performs Skia rasterization on the GPU.

IO Task Runner : Handles I/O‑bound work such as image decoding.

When a PlatformView is present, the Platform and Raster task runners must be merged so that a single thread consumes tasks from both queues.

Problem Analysis

The author encountered crashes in two scenarios:

Independent multi‑engine setup (separate engines for each view) – the raster thread merger only supports a one‑to‑one merge, causing a SIGABRT when a second engine attempts to merge.

Lightweight multi‑engine setup (engine groups) – the Platform and Raster queues are shared across engines, but each engine creates its own RasterThreadMerger , leading to a failed ownership check and another abort.

Log excerpts show the failure point in MessageLoopTaskQueues::Merge where the function returns false because a queue is already owned by another merger.

Root Cause

Both failures stem from the engine assuming a single owner for a task queue. The original implementation stores a single owner_of ID, preventing a one‑to‑many relationship required for multiple engines sharing the same Platform thread.

Solution Overview

The proposed fix introduces a true one‑to‑many merge model:

Change TaskQueueEntry::owner_of from a single TaskQueueId to a std::set<TaskQueueId> to track multiple owners.

Update MessageLoopTaskQueues::PeekNextTaskUnlocked to iterate over all owned queues and select the earliest task.

Introduce a SharedThreadMerger that is shared among engines with the same Platform/Raster pair, removing the ownership check in RasterThreadMerger .

Adjust merge/unmerge APIs ( MergeWithLease , UnMergeNow , DecrementLease , IsMerged ) to delegate to the shared merger and maintain a lease counter per caller.

Handle edge cases such as delayed unmerge when some engines still need the merged state.

Key code changes include:

class TaskQueueEntry {
  std::set
owner_of; // previously TaskQueueId owner_of;
};
TaskSource::TopTask MessageLoopTaskQueues::PeekNextTaskUnlocked(TaskQueueId owner) const {
  if (entry->owner_of.empty()) {
    return entry->task_source->Top();
  }
  // iterate over all owned queues and pick the earliest task
  for (TaskQueueId subsumed : entry->owner_of) {
    // compare tasks ...
  }
}

Additional adjustments were made to the rasterizer setup to create or share a RasterThreadMerger only when appropriate, and to clear GL contexts after a merge/unmerge transition.

Implementation Details and Pitfalls

Several practical issues were encountered during development:

When embedding multiple FlutterFragment s, the default transparent surface caused Z‑order conflicts; the fix was to use opaque surfaces.

On iOS, unit tests required launching the Xcode test runner to capture crashes.

Global static variables holding non‑trivial objects were replaced with member variables in the shell class to satisfy Google C++ style guidelines.

A Windows‑specific test failure was traced to the initialization order of a std::thread member before a synchronization primitive; reordering the members resolved the issue.

Final Pull Request

The changes were merged into the official Flutter engine in Pull Request #27662 , enabling reliable PlatformView usage across multiple engines.

Takeaways for Contributing to Open‑Source

When contributing fixes, include tests, follow the project’s coding style, and provide clear documentation. Automated formatting tools (e.g., dart ci/bin/format.dart -f ) help keep the codebase consistent.

FlutterMulti-Engineengine internalsThread Mergingplatformview
ByteDance Terminal Technology
Written by

ByteDance Terminal Technology

Official account of ByteDance Terminal Technology, sharing technical insights and team updates.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.