Resolving Thread Merging Issues for PlatformView in Multi‑Engine Flutter Applications
This article explains the thread‑merging problem that arises when using PlatformView in multi‑engine Flutter scenarios, analyzes its root causes in both independent and lightweight engines, and presents a comprehensive solution—including code changes, task‑queue merging logic, and practical implementation details — that has been merged into the official Flutter engine repository.
This article introduces the unavoidable thread‑merging issue that occurs when using PlatformView in a multi‑engine Flutter environment and presents the final solution that has been merged into the official Flutter engine repository.
Background
PlatformView is a mechanism that allows Flutter to embed native platform views (e.g., WebView, map controls, third‑party SDKs) as Flutter widgets, enabling a unified cross‑platform interface.
Example Flutter code using a WebView:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
// .. omitted App code
class _BodyState extends State<Body> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('InAppWebView Example'),
),
body: Expanded(
child: WebView(
initialUrl: 'https://flutter.dev/',
javascriptMode: JavascriptMode.unrestricted,
),
),
);
}
}Flutter historically provided two PlatformView implementations: VirtualDisplay (pre‑1.20) and HybridComposition (recommended after 1.20).
Flutter Engine Thread Model
The engine defines four Task Runners, each typically bound to a separate OS thread:
Task Runner
Purpose
Platform Task Runner
Handles UI events, PlatformChannel messages, and dispatches them to other runners.
UI Task Runner
Runs Dart code and builds the layer tree.
GPU (Raster) Task Runner
Executes Skia drawing on the GPU.
IO Task Runner
Performs time‑consuming I/O operations such as image decoding.
When a PlatformView is present, the Platform and Raster threads must be merged so that a single runner consumes tasks from both queues.
Thread Merging Problem
In multi‑engine scenarios the original merge logic only supports a one‑to‑one relationship, causing crashes when multiple engines attempt to merge with the same Platform thread.
Key C++ snippet from external_view_embedder.cc that performs the merge:
// src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc
PostPrerollResult AndroidExternalViewEmbedder::PostPrerollAction(
fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
if (!FrameHasPlatformLayers()) {
// No platform view in this frame.
return PostPrerollResult::kSuccess;
}
if (!raster_thread_merger->IsMerged()) {
// Merge the raster thread with the platform thread.
raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration);
CancelFrame();
return PostPrerollResult::kSkipAndRetryFrame;
}
// Extend lease if needed.
raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
if (previous_frame_view_count_ == 0) {
return PostPrerollResult::kResubmitFrame;
}
return PostPrerollResult::kSuccess;
}The merge fails when the engine attempts a second merge because the internal Merge function detects that a task queue already has an owner.
bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {
if (owner == subsumed) return true;
std::mutex& owner_mutex = GetMutex(owner);
std::mutex& subsumed_mutex = GetMutex(subsumed);
std::scoped_lock lock(owner_mutex, subsumed_mutex);
auto& owner_entry = queue_entries_.at(owner);
auto& subsumed_entry = queue_entries_.at(subsumed);
if (owner_entry->owner_of == subsumed) return true;
std::vector<TaskQueueId> keys = {owner_entry->owner_of, owner_entry->subsumed_by,
subsumed_entry->owner_of, subsumed_entry->subsumed_by};
for (auto key : keys) {
if (key != _kUnmerged) {
return false; // merge not allowed
}
}
owner_entry->owner_of = subsumed;
subsumed_entry->subsumed_by = owner;
if (HasPendingTasksUnlocked(owner)) {
WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner));
}
return true;
}Log output shows the merge being attempted twice, with the second attempt failing:
E/flutter: ::Merge() called with owner=0, subsumed=2
E/flutter: [0]=18446744073709551615 [1]=18446744073709551615 ...
E/flutter: ::Merge() called with owner=0, subsumed=5
E/flutter: [0]=2 [1]=18446744073709551615 ...
A/flutter: Check failed: success. Unable to merge the raster and platform threads.Analysis of Independent vs. Lightweight Engines
Independent engines keep each of the four threads separate, while lightweight engines share the Platform and Raster queues across engines. This sharing leads to the merge check failing immediately because the queues are already merged.
Proposed Solution
The solution introduces a shared RasterThreadMerger that can handle one‑to‑many merges by tracking multiple owners using a std::set<TaskQueueId> instead of a single ID.
Modified TaskQueueEntry definition:
class TaskQueueEntry {
public:
// Set of TaskQueueIds owned by this queue.
std::set<TaskQueueId> owner_of; // previously TaskQueueId owner_of;
// ... other members
};Updated logic for fetching the next task now iterates over all owned queues:
TaskSource::TopTask MessageLoopTaskQueues::PeekNextTaskUnlocked(TaskQueueId owner) const {
const auto& entry = queue_entries_.at(owner);
if (entry->owner_of.empty()) {
FML_CHECK(!entry->task_source->IsEmpty());
return entry->task_source->Top();
}
std::optional<TaskSource::TopTask> top_task;
auto top_task_updater = [&top_task](const TaskSource* source) {
if (source && !source->IsEmpty()) {
TaskSource::TopTask other = source->Top();
if (!top_task.has_value() || top_task->task > other.task) {
top_task.emplace(other);
}
}
};
top_task_updater(entry->task_source.get());
for (TaskQueueId subsumed : entry->owner_of) {
top_task_updater(queue_entries_.at(subsumed)->task_source.get());
}
FML_CHECK(top_task.has_value());
return top_task.value();
}Engine setup now creates or shares a merger only when the Platform and Raster queues differ:
void Rasterizer::Setup(std::unique_ptr<Surface> surface) {
if (external_view_embedder_ && external_view_embedder_->SupportsDynamicThreadMerging() && !raster_thread_merger_) {
const auto platform_id = delegate_.GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId();
const auto gpu_id = delegate_.GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId();
raster_thread_merger_ = fml::RasterThreadMerger::CreateOrShareThreadMerger(
delegate_.GetParentRasterThreadMerger(), platform_id, gpu_id);
}
if (raster_thread_merger_) {
raster_thread_merger_->SetMergeUnmergeCallback([=]() {
if (surface_) {
surface_->ClearRenderContext();
}
});
}
}Additional changes include:
Removing the static block_merging guard that prevented multiple merges.
Introducing a SharedThreadMerger that proxies calls from individual engines and maintains a reference count ( std::map<ThreadMergerCaller, int>) to manage lease terms.
Ensuring unmerge only occurs when the last engine releases the merger.
Updating unit tests for Windows, macOS, and Linux to cover the new multi‑merge behavior.
Implementation Pitfalls
Several platform‑specific issues were encountered, such as Z‑order conflicts in Android FlutterSurfaceView, ordering of static members causing crashes on Windows, and ImageReader creation failures when the keyboard resized a WebView.
Example of a problematic TaskQueueWrapper on Windows:
struct TaskQueueWrapper {
fml::MessageLoop* loop = nullptr;
// This field must be declared before latch and term because of initialization order.
std::thread thread;
fml::AutoResetWaitableEvent latch;
fml::AutoResetWaitableEvent term;
TaskQueueWrapper()
: thread([this]() {
fml::MessageLoop::EnsureInitializedForCurrentThread();
loop = &fml::MessageLoop::GetCurrent();
latch.Signal();
term.Wait();
}) {
latch.Wait();
}
// ... destructor omitted
};Final Pull Request
The complete changes have been merged into the official Flutter engine repository:
https://github.com/flutter/engine/pull/27662Contribution Experience
Key take‑aways for contributing to the Flutter project include creating an issue before a PR, adding comprehensive tests, adhering to the Google C++ style guide, and using the provided formatting tools to automatically fix style violations.
Conclusion
The work resolves the multi‑engine PlatformView thread‑merging limitation, improves stability for both independent and lightweight Flutter engines, and demonstrates a systematic approach to diagnosing and fixing low‑level engine bugs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ByteDance Dali Intelligent Technology Team
Technical practice sharing from the ByteDance Dali Intelligent Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
