作者:字节移动技术-李皓骅
摘要
本文介绍了 Flutter 多引擎下,使用 PlatformView 场景时不能绕开的一个线程合并问题,以及它最终的解决方案。最终 Pull Request 已经 merge 到 Google 官方 Flutter 仓库:
https://github.com/flutter/engine/pull/27662
本文关键点:
- 线程合并,实际上指的并不是操作系统有什么高级接口,可以把两个 pthread 合起来,而是 flutter 引擎中的四大 Task Runner 里,用一个 Task Runner 同时消费处理两个 Task Queue 中排队的任务。
- 线程合并问题,指的是 Flutter 引擎四大线程(Platform 线程、UI 线程、Raster 线程、IO 线程)其中的 Platform 线程和 Raster 线程在使用 PlatformView 的场景时需要合并和分离的问题。之前的官方的线程合并机制,只支持一对一的线程合并,但多引擎场景就需要一对多的合并和一些相关的配套逻辑。具体请看下文介绍。
- 关于 Flutter 引擎的四大 Task Runner 可以参考官方 wiki 中的 Flutter Engine 线程模型 : https://github.com/flutter/flutter/wiki/The-Engine-architecture#threading
- 本文介绍的线程合并操作(也就实现了一个 looper 消费两个队列的消息的效果),见如下的示意图,这样我们可以有个初步的印象:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FIt1i6la-1631187770198)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/0.png)]
背景介绍
什么是 PlatformView?
首先,介绍下 PlatformView 是什么,其实它简单理解成——平台相关的 View 。也就是说,在Android 和 iOS 平台原生有这样的控件,但是在Flutter的跨平台控件库里没有实现过的一些Widget,这些控件我们可以使用Flutter提供的PlatformView的机制,来做一个渲染和桥接,并且在上层可以用Flutter的方法去创建、控制这些原生View,来保证两端跨平台接口统一。
比如WebView,地图控件,第三方广告SDK等等这些场景,我们就必须要用到PlatformView了。
举一个例子,下图就是 Android 上使用 PlatformView 机制的 WebView 控件和 Flutter控件的混合渲染的效果:
可以看到Android ViewTree上确实存在一个WebView。
下面是一个Flutter的使用WebView的上层代码示例:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
// .. 省略App代码
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,
),
),
);
}
}
黄色背景内容是使用WebView的方法,可以看到,经过 WebView 插件的封装,虽然背后是 Android 平台或者 iOS 平台本身的 WebView,但是就像使用 Flutter Widget 一样方便。
其实在Flutter历史演进过程中,对于 PlatformView 的处理曾经有过两种方案,分别是:
Flutter 1.20版本之前的 VirtualDisplay 方式,和 Flutter 1.20 之后推荐使用的 HybridComposition 方式。现在官方推荐 HybridComposition 的 embedding 方式,可以避免很多之前的 bug 和性能问题,具体不再赘述,可以参考官方文档。
官方的PlatformView介绍文档:在 Flutter 应用中使用集成平台视图托管您的原生 Android 和 iOS 视图
Flutter 引擎线程模型
要理解下文的线程合并,首先我们需要了解下Flutter 引擎的线程模型。
Flutter Engine 需要提供4个 Task Runner,这4个 Runner 默认的一般情况下分别对应分别着4个操作系统线程,这四个 Runner 线程各司其职:
Task Runner | 作用 |
---|---|
Platform Task Runner | App 的主线程,用于处理用户操作、各类消息和 PlatformChannel ,并将它们传递给其他 Task Runner 或从其他 Task Runner 传递过来。 |
UI Task Runner | Dart VM 运行所在的线程。运行 Dart 代码的线程,负责生成要传递给 Flutter 引擎的 layer tree。 |
GPU Task Runner (Raster Task Runner) | 与 GPU 处理相关的线程。它是使用 Skia 最终绘制的过程相关的线程(OpenGL 或 Vulkan 等等) |
IO Task Runner | 执行涉及 I/O 访问的耗时过程的专用线程,例如解码图像文件。 |
如下图所示:
线程合并
关于线程合并,我们可能有下面几个疑问:
- 为什么不用 platform view 的时候,两种多引擎工作的好好的?
- 为什么使用 platform view 的时候,iOS 和 Android 两端,都需要 merge 么,能不能不 merge ?
- merge 以后,在不使用 platform view 的 flutter 页面里,还会取消 merge 还原回来么?
我们来怀着这几个疑问去分析问题。
为什么要线程合并?
为什么在使用PlatformView的时候,需要把 Platform 线程和 Raster 线程合并起来?
简单的说就是:
- 所有 PlatformView 的操作需要在主线程里进行(Platform线程指的就是App的主线程),否则在 Raster 线程处理 PlatformView 的 composition 和绘制等操作时,Android Framework 检查到非 App 主线程,会直接抛异常;
- Flutter 的 Raster渲染操作和 PlatformView 的渲染逻辑是各自渲染的,当他们一起使用的时候每一帧渲染时候,需要做同步,而比较简单直接的一种实现方式就是把两个任务队列合并起来,只让一个主线程的 runner 去逐个消费两个队列的任务;
- Skia和GPU打交道的相关操作,其实是可以放在任意线程里的,合并到App主线程进行相关的操作是完全没有问题的
那么,Platform Task Runner在合并GPU Task Runner后,主线程也就包揽并承担了原本两个Runner的所有任务,参考下面的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YD2wkTFV-1631187770203)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/3.png)]
我们分析external_view_embedder.cc相关的代码也可以看到合并的操作:
// src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc
// |ExternalViewEmbedder|
PostPrerollResult AndroidExternalViewEmbedder::PostPrerollAction(
fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
if (!FrameHasPlatformLayers()) {
// 这里判断当前frame有没有platform view,有就直接返回
return PostPrerollResult::kSuccess;
}
if (!raster_thread_merger->IsMerged()) {
// 如果有platform view并且没merger,就进行merge操作
// The raster thread merger may be disabled if the rasterizer is being
// created or teared down.
//
// In such cases, the current frame is dropped, and a new frame is attempted
// with the same layer tree.
//
// Eventually, the frame is submitted once this method returns `kSuccess`.
// At that point, the raster tasks are handled on the platform thread.
raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration);
CancelFrame();
return PostPrerollResult::kSkipAndRetryFrame;
}
// 扩展并更新租约,使得后面没有platform view并且租约计数器降低到0的时候,开始unmerge操作
raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
// Surface switch requires to resubmit the frame.
// TODO(egarciad): https://github.com/flutter/flutter/issues/65652
if (previous_frame_view_count_ == 0) {
return PostPrerollResult::kResubmitFrame;
}
return PostPrerollResult::kSuccess;
}
也就是说,我们有两种情况,一种是当前layers中没有 PlatformView ,一种是开始有PlatformView,我们分析下各自的四大线程的运行状态:
- 首先没有PlatformView的时候的情况下,四大 Task Runner 的状态:
Platform ✅ / UI ✅ / Raster ✅ / IO ✅
- 使用PlatformView的时候的情况下,四大 Task Runner 的状态:
Platform ✅(同时处理Raster线程的任务队列) / UI ✅ / Raster ❌(闲置) / IO ✅
merge 和 unmerge 操作,可以如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYWxLtuO-1631187770204)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/4.png)]
一个 runner 如何消费两个任务队列?
关键的两个点就是:
- TaskQueueEntry 类中有两个成员变量,记录了当前队列的上游和下游的queue_id
- 在 TaskQueueRunner 取下一个任务的时候(也就是
PeekNextTaskUnlocked
函数)做了特殊处理:
TaskQueueEntry类的这两个成员的声明和文档:
/// A collection of tasks and observers associated with one TaskQueue.
///
/// Often a TaskQueue has a one-to-one relationship with a fml::MessageLoop,
/// this isn't the case when TaskQueues are merged via
/// \p fml::MessageLoopTaskQueues::Merge.
class TaskQueueEntry {
public:
// ....
std::unique_ptr<TaskSource> task_source;
// Note: Both of these can be _kUnmerged, which indicates that
// this queue has not been merged or subsumed. OR exactly one
// of these will be _kUnmerged, if owner_of is _kUnmerged, it means
// that the queue has been subsumed or else it owns another queue.
TaskQueueId owner_of;
TaskQueueId subsumed_by;
// ...
};
取下一个任务的PeekNextTaskUnlocked
的逻辑(参考注释):
// src/flutter/fml/message_loop_task_queues.cc
const DelayedTask& MessageLoopTaskQueues::PeekNextTaskUnlocked(
TaskQueueId owner,
TaskQueueId& top_queue_id) const {
FML_DCHECK(HasPendingTasksUnlocked(owner));
const auto& entry = queue_entries_.at(owner);
const TaskQueueId subsumed = entry->owner_of;
if (subsumed == _kUnmerged) { // 如果没merge的话,就取自己当前的top任务
top_queue_id = owner;
return entry->delayed_tasks.top();
}
const auto& owner_tasks = entry->delayed_tasks;
const auto& subsumed_tasks = queue_entries_.at(subsumed)->delayed_tasks;
// we are owning another task queue
const bool