作者:字节移动技术-李皓骅
摘要
本文介绍了 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 消费两个队列的消息的效果),见如下的示意图,这样我们可以有个初步的印象:
背景介绍
什么是 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 线程各司其职:
如下图所示:
线程合并
关于线程合并,我们可能有下面几个疑问:
为什么不用 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 的所有任务,参考下面的示意图:
我们分析 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 操作,可以如下图所示:
一个 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 subsumed_has_task = !subsumed_tasks.empty();
const bool owner_has_task = !owner_tasks.empty();
if (owner_has_task && subsumed_has_task) {
const auto owner_task = owner_tasks.top();
const auto subsumed_task = subsumed_tasks.top();
// 如果merge了的话,根据标记判断,就取两个队列的top任务,再比较谁比较靠前
if (owner_task > subsumed_task) {
top_queue_id = subsumed;
} else {
top_queue_id = owner;
}
} else if (owner_has_task) {
top_queue_id = owner;
} else {
top_queue_id = subsumed;
}
return queue_entries_.at(top_queue_id)->delayed_tasks.top();
}
复制代码
问题与分析
遇到的问题
我们在使用官方引擎的过程中,分别在独立多引擎和轻量级多引擎两个场景下的 PlatformView 时,都遇到了线程合并的问题。
问题 1:独立多引擎下的线程合并问题
最早是 webview 的业务方报告的 slardar 崩溃问题,当时写了一个 unable_to_merge_raster_demo 的例子,然后给官方提交了一个 issue:
https://github.com/flutter/flutter/issues/78946
也就是说,在独立的多引擎下,使用 platform view 的时候,会因为 raster_thread_merger 不支持多于一对一的合并(merge)操作而失败并报错。
崩溃的 demo:
https://github.com/eggfly/unable_to_merge_raster_demo
看日志这是一个崩溃,然后接一个 native 的 SIGABRT 崩溃,日志如下:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/umi/umi:11/RKQ1.200826.002/21.3.3:user/release-keys'
Revision: '0'
ABI: 'arm64'
pid: 11108, tid: 11142, name: 1.raster >>> com.example.unable_to_merge_raster_demo <<<
uid: 10224
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: '[FATAL:flutter/fml/raster_thread_merger.cc(48)] Check failed: success. Unable to merge the raster and platform threads
x0 0000000000000000 x1 0000000000002b86 x2 0000000000000006 x3 0000007c684fd150
// ... register values
backtrace:
#00 pc 0000000000089acc /apex/com.android.runtime/lib64/bionic/libc.so (abort+164) (BuildId: a790cdbd8e44ea8a90802da343cb82ce)
#01 pc 0000000001310784 /data/app/~~W2sUpMihWXQXs-Yx0cuHWg==/com.example.unable_to_merge_raster_demo-IUwY4BX5gBqjR0Pxu09Pfw==/lib/arm64/libflutter.so (BuildId: 854273bae6db1c10c29f7189cb0cf640ad4db110)
#02 pc 000000000133426c /data/app/~~W2sUpMihWXQXs-Yx0cuHWg==/com.example.unable_to_merge_raster_demo-IUwY4BX5gBqjR0Pxu09Pfw==/lib/arm64/libflutter.so (BuildId: 854273bae6db1c10c29f7189cb0cf640ad4db110)
// ... more stack frames
Lost connection to device.
复制代码
问题 2:轻量级多引擎下的线程合并问题
Flutter 2.0 版本后引入了 lightweight flutter engines,也就是轻量级引擎,可以通过 FlutterEngineGroups 和 spawn()函数来生成一个轻量级引擎,官方轻量级相关的提交:
https://github.com/flutter/engine/pull/22975
我们在用官方的 lightweight multiple engine 的 sample 代码的时候,尝试在多引擎下加上 PlatformView,也就是在 main.dart 里加上 webview。
官方 demo 代码:https://github.com/flutter/samples/tree/master/add_to_app/multiple_flutters
运行起来会有这样的崩溃日志,这里的错误和问题 1 有一点区别:
[FATAL:flutter/fml/raster_thread_merger.cc(22)] Check failed: !task_queues_->Owns(platform_queue_id_, gpu_queue_id_).
复制代码
问题分析
分析 1:独立多引擎线程合并问题
问题 1 是 Flutter 1.22+独立引擎的问题,我在代码中搜索raster_thread_merger.cc(48)] Check failed: success. Unable to merge the raster and platform threads
其中 raster_thread_merger.cc 的 48 行这样的代码:
当success == false
的时候会触发 SIGABRT,看 Merge()函数什么时候返回 false:
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> owner_subsumed_keys = {
owner_entry->owner_of, owner_entry->subsumed_by, subsumed_entry->owner_of,
subsumed_entry->subsumed_by};
for (auto key : owner_subsumed_keys) {
if (key != _kUnmerged) {
return false; // <--- 这里是返回false唯一的可能
}
}
owner_entry->owner_of = subsumed;
subsumed_entry->subsumed_by = owner;
if (HasPendingTasksUnlocked(owner)) {
WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner));
}
return true;
}
复制代码
Merge 函数看起来是把两个 task_queue 合并到一起的关键逻辑,通过设置 entry->owner_of 和 subsumed_by 来实现的。参考上面 TaskQueueEntry 类的声明代码。
那么在 owner_subsumed_keys 这个 vector 的四个元素里打上 log 看一下,for 循环的本意是检查 owner 和上游和下游,以及 subsumed 的上游和下游,加起来这四个 id 的任意元素里如果出现一个不等于_kUnmerged
的就会检查失败,进而不进行后面的 merge 和赋值操作,直接返回 false。
通过 log 可以看出:
E/flutter: ::Merge() called with owner=0, subsumed=2
E/flutter: [0]=18446744073709551615 [1]=18446744073709551615 [2]=18446744073709551615 [3]=18446744073709551615
E/flutter: ::Merge() called with owner=0, subsumed=5
E/flutter: [0]=2 [1]=18446744073709551615 [2]=18446744073709551615 [3]=18446744073709551615
A/flutter: Check failed: success. Unable to merge the raster and platform threads.
复制代码
可以看到 Merge 调用了两次,并且第二次调用的第 0 个元素是 2,印证了上面 for 循环出现不等于 unmerge 常量的情况了。
其中的 2 和 5 分别是引擎 1 和引擎 2 的 raster 线程,通过
adb root
adb shell kill -3 $pid
复制代码
再 adb pull /data/anr/trace_00 拉出来看真实的线程也可以看到1.ui, 2.ui, 1.raster, 2.raster, 1.io, 2.io
这样的被设置了名字线程(有 pthread_setname 之类的函数):
在 Google 搜索这个Unable to merge the raster and platform threads
在也可以搜到一个提交:
https://github.com/flutter/engine/pull/23733
提交介绍说:
This will make sure that people don't use platform view with flutter engine groups until we've successfully accounted for them.
所以它在做第 1 次 merge 的时候,设置了block_merging
标记,第二次以及后面的 merge 操作会失败并打印一个日志:
所以,在官方那是一个 todo,是待实现的 feature。
分析 2:轻量级多引擎线程合并问题
问题 2 是 Flutter 2.0+轻量级引擎下的问题,直接看轻量级多引擎下,检查失败的那一行的源码:
很明显,和上面的独立多引擎不同,这里在创建 RasterThreadMerger 的构造函数的 FML_CHECK 检查就失败了,证明 platform 和 raster 已经是 merge 的状态了,所以这里也是 SIGABRT 并且程序退出了。
通过打印 log 看到两个引擎的 platform 和 raster 的 id 是共享的,引擎 1 和引擎 2 的 platform_queue_id 都是 0,raster_queue_id 都是 2
小结:多对一合并是官方未实现的 feature
很容易我们可以推理得到,多引擎的每个引擎都需要有一套四大线程,它们可以选择公用,或者也可以选择创建自己独立的线程。
我们通过之前的 log 打印的 task_queue_id,分析一下两个问题唯一的区别:
在问题 1(两个独立引擎中)的情况是这样的(四大线程除了 platform,其他三个线程不共享):
在问题 2(两个轻量级引擎中)的情况是这样的(四大线程全部共享):
所以相对来讲,感觉问题 2 更容易解决,并且我们使用 flutter 2.0 和卡片方案的业务,马上就将要遇到这个问题。
官方的轻量级引擎有一个 TODO 列表,把这个问题标记成 Cleanup 的任务:
https://github.com/flutter/flutter/issues/72009
官方标记了 P5 优先级:
因为业务需要所以直接就不等了,我们干脆自己实现它。
线程合并解决方案
快速解决问题 2:解决轻量级引擎的问题
既然在轻量级引擎下,platform 线程和 raster 线程都是共享的,只是 engine 和 rasterizer 的对象是分开的,并且现在的逻辑是分别在两个引擎里,new 了自己的 RasterThreadMerger 对象,进行后续的 merge 和 unmerge 操作。并且在 merge 的时候做是否 Owns 的检查。
那我们可以简单的做这几件事:
改成去掉 Owns() 的检查和相关线程检查
共享一个 RasterThreadMerger 对象进行 merge 和 unmerge 操作
先不管那个 lease_term (租约)计数器,留下后续处理
修改方案基本是坤神(我们 Flutter 组的战友)的 prototype 提交的方案,并且加一些边角的处理即可。
Prototype 原型的关键修改的地方:
每个带 title 的都是一个 FlutterView,终于不崩溃了:
效果截图:
但是这只是一个原型,很多状态问题和 merge 的逻辑我们没有处理的很好,问题包括:
我们不能像原型一样,在字节的 Flutter 引擎里直接 hardcode 写死共享一个 merger 对象,所以 2.0 之前的独立多引擎仍旧会有问题
我们没正确处理 IsMerged 函数的正确返回结果
我们还没有正确处理 lease_term 的计数器,lease_term 计数器降到 0 的时候,应该 unmerge
我们假象有一种 case: 引擎 1 需要 unmerge,但是引擎 2 还需要渲染 platformview,这时候 1 的 unmerge 不能立刻调用,需要等所有引擎都没有 merge 的需求的时候,再去把 platform 和 raster 脱离开
所以我们需要有一套真正的终极解决方案,最好能:覆盖两个 raster 同时 merge 到一个 platform 的情况,然后贡献给官方。
彻底解决问题 1 和 2(最终方案)
解决思路
经过查看代码里raster_thread_merger
对象是rasterizer
的一个成员:
// src/flutter/shell/common/rasterizer.h
namespace flutter {
//----------------------------------------------------------------------------
class Rasterizer final : public SnapshotDelegate {
public:
//-------
private:
// ...省略
fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger_;
复制代码
以下都是 RasterThreadMerger 类里的成员函数,都是需要我们修改成一对多 merge 以后,也保证去维护正常调用时机的 API:
// src/flutter/fml/raster_thread_merger.h
#ifndef FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_
#define FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_
// ... 省略 #include
namespace fml {
class RasterThreadMerger
: public fml::RefCountedThreadSafe<RasterThreadMerger> {
public:
// Merges the raster thread into platform thread for the duration of
// the lease term. Lease is managed by the caller by either calling
// |ExtendLeaseTo| or |DecrementLease|.
// When the caller merges with a lease term of say 2. The threads
// are going to remain merged until 2 invocations of |DecreaseLease|,
// unless an |ExtendLeaseTo| gets called.
//
// If the task queues are the same, we consider them statically merged.
// When task queues are statically merged this method becomes no-op.
void MergeWithLease(size_t lease_term);
// Un-merges the threads now, and resets the lease term to 0.
//
// Must be executed on the raster task runner.
//
// If the task queues are the same, we consider them statically merged.
// When task queues are statically merged, we never unmerge them and
// this method becomes no-op.
void UnMergeNow();
// If the task queues are the same, we consider them statically merged.
// When task queues are statically merged this method becomes no-op.
void ExtendLeaseTo(size_t lease_term);
// Returns |RasterThreadStatus::kUnmergedNow| if this call resulted in
// splitting the raster and platform threads. Reduces the lease term by 1.
//
// If the task queues are the same, we consider them statically merged.
// When task queues are statically merged this method becomes no-op.
RasterThreadStatus DecrementLease();
bool IsMerged();
// ... 省略一些接口
bool IsMergedUnSafe() const;
};
} // namespace fml
#endif // FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_
复制代码
merger 创建的时候,需要考虑某些情况下不支持 merger 需要保持 merger 不被创建出来(比如某些不支持的平台或者某些 unittest):
// src/flutter/shell/common/rasterizer.cc
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([=]() {
// Clear the GL context after the thread configuration has changed.
if (surface_) {
surface_->ClearRenderContext();
}
});
}
}
复制代码
那么我们有一种选择是在每个 engine 各自的 rasterizer 的创建的时候,改改它的逻辑,在 raster_queue_id 相同的时候,复用之前的对象,听起来是个好办法。
实现的方案
画了个图作为两种情况的展示:
关于线程什么情况下允许合并,什么情况下不允许合并的示意图:
另外还有一种情况没有列出,自己 merge 到自己的情况:现在的代码默认返回 true 的。
总结一句话就是一个 queue 可以合并多个 queue(可以有多个下游),但是一个 queue 不可以有多个上游。
此实现的设计:
首先最重要的:将 TaskQueueEntry 中的成员owner_of
从TaskQueueId
改成std::set<TaskQueueId> owner_of
,来记录这个线程所有 merge 过的 subsumed_id(一对多的 merge 关系)
代码的修改平台独立的,可以让 Android 和 iOS 共享相同的代码逻辑,确保不同平台的相关目录中的代码没有被更改(之前做过一个版本的方案,是 Android、iOS 分别修改了 embedder 类的逻辑)
删除了之前官方禁用阻塞逻辑的代码(也就是 revert 了官方之前的这个提交:https://github.com/flutter/engine/pull/23733)
为了减少现有代码的更改数量,把旧的 RasterThreadMerger 类视为 proxy,并引入了一个新的 SharedThreadMerger 类,并且在引擎里记录 parent_merger,在引擎的 spawn 函数里拿到父亲引擎的 merger,看是否可以共享
与 merge 相关的方法调用(包括 MergeWithLease()、UnmergeNow()、DecrementLease()、IsMergeUnsafe()改成重定向到 SharedThreadMerger 内的方法,然后用一个std::map<ThreadMergerCaller, int>
来记录合并状态和 lease_term 租约计数器
将 UnMergeNow()更改为 UnMergeNowIfLastOne(),以记住所有 merge 的调用者,在调用 Rasterizer::Teardown()的时候,并且它是在最后一个 merger 的时候,立刻 unmerge,其他情况需要保持 unmerge 状态。
在 shell_unittest 和 fml_unittests 中添加了更多的测试,并在 run_tests.py 中启用 fml_unittests(之前被一个官方提交禁用了,发现改什么代码都不起作用,比较坑)
解决方案相关的代码
TaskQueueEntry 改成 std::set 的集合
class TaskQueueEntry {
public:
/// 省略
/// Set of the TaskQueueIds which is owned by this TaskQueue. If the set is
/// empty, this TaskQueue does not own any other TaskQueues.
std::set<TaskQueueId> owner_of; // 原来是TaskQueueId owner_of;
复制代码
PeekNextTaskUnlocked
新的逻辑:
// src/flutter/fml/message_loop_task_queues.cc
TaskSource::TopTask MessageLoopTaskQueues::PeekNextTaskUnlocked(
TaskQueueId owner) const {
FML_DCHECK(HasPendingTasksUnlocked(owner));
const auto& entry = queue_entries_.at(owner);
if (entry->owner_of.empty()) {
FML_CHECK(!entry->task_source->IsEmpty());
return entry->task_source->Top();
}
// Use optional for the memory of TopTask object.
std::optional<TaskSource::TopTask> top_task;
// 更新当前最小的任务的lambda函数
std::function<void(const TaskSource*)> top_task_updater =
[&top_task](const TaskSource* source) {
if (source && !source->IsEmpty()) {
TaskSource::TopTask other_task = source->Top();
if (!top_task.has_value() || top_task->task > other_task.task) {
top_task.emplace(other_task);
}
}
};
TaskSource* owner_tasks = entry->task_source.get();
top_task_updater(owner_tasks);
for (TaskQueueId subsumed : entry->owner_of) {
TaskSource* subsumed_tasks = queue_entries_.at(subsumed)->task_source.get();
// 遍历set中subsumed合并的任务队列,更新当前最小的任务
top_task_updater(subsumed_tasks);
}
// At least one task at the top because PeekNextTaskUnlocked() is called after
// HasPendingTasksUnlocked()
FML_CHECK(top_task.has_value());
return top_task.value();
}
复制代码
merge 和 unmerge 相关的检查(省略,详情可以参考 Pull Request 中代码提交)
实现过程中的小坑
和官方一样,使用 FlutterFragment 的方式来嵌入多引擎的时候,FlutterSurfaceView 会给 surface 设置 ZOrder,这时候多个 Surface 会有 ZOrder 争抢 top 的问题
private void init() {
// If transparency is desired then we'll enable a transparent pixel format and place
// our Window above everything else to get transparent background rendering.
if (renderTransparently) {
getHolder().setFormat(PixelFormat.TRANSPARENT);
setZOrderOnTop(true);
}
复制代码
需要在创建的时候,去掉 Transparent 的 flag,需要这样改:(这个问题被坑了很久,差点没让我放弃这个提交)
val flutterFragment =
FlutterFragment.withCachedEngine(i.toString())
// Opaque is to avoid platform view rendering problem due to wrong z-order
.transparencyMode(TransparencyMode.opaque) // this is needed
.build<FlutterFragment>()
复制代码
在 iOS 做 unittest 的时候,发现有相应的崩溃,也是没有崩溃的 stack 和详细 log,后来发现 iOS 目录下有一个 README,提到了使用 xcode 可以打开 unittest 工程,开启模拟器自动测试,并且发现可以直接在我没有 attach 的情况下,自动 attach lldb 并且定位到崩溃的那一行代码:
官方 review 代码的时候,提出的最大问题是之前用了 map 做一个全局 static 的std::map<Pair<QueueId, QueueId>, SharedThreadMerger>
的字典 static 变量,用来取 platform&raster 这一个 pair 的 merger,但是老外扔给我一个 google c++规范,明确写了 non-trivial 的类型才允许保存为全局变量,官方规范文档:https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables
最终通过把 merger 作为 Shell 类的成员变量来解决这个生命周期的问题。
在测试的时候发现 macOS、Linux 的 engine 的 unopt 目标的 build 和 test 都没问题,但是偏偏 windows 的引擎去测试 host_debug_unopt 的 unittest 的时候,出直接 exit,exitcode 不是 0 的
然后 windows 的崩溃栈默认不会打印到 terminal:谷歌的 luci 平台上的失败信息:
可以看到什么 log 都没有。
困扰半天最终决定:装一个 windows 虚拟机!神奇的事情发生了,在我的 windows 10 + flutter engine 环境下编译然后运行我的 test,结果全都过了。惊愕!最终还是通过两分法看修改,定位到了一个 unittest 的抽取的改法造成了问题。
留个题目:可以看出如下代码为什么 windows 会有问题吗?
/// A mock task queue NOT calling MessageLoop->Run() in thread
struct TaskQueueWrapper {
fml::MessageLoop* loop = nullptr;
/// 问题提示在这里:
/// This field must below latch and term member, because
/// cpp standard reference:
/// non-static data members are initialized in the order they were declared in
/// the class definition
std::thread thread;
/// The waiter for message loop initialized ok
fml::AutoResetWaitableEvent latch;
/// The waiter for thread finished
fml::AutoResetWaitableEvent term;
TaskQueueWrapper()
: thread([this]() {
fml::MessageLoop::EnsureInitializedForCurrentThread();
loop = &fml::MessageLoop::GetCurrent();
latch.Signal();
term.Wait();
}) {
latch.Wait();
}
// .. 省略析构函数, term.Signal() 和 thread.join() 等等
};
复制代码
跑起来两个 webview 的 demo 以后,点下面的键盘,会有一个 crash(下面的界面弹出键盘以后就崩了):
结果是 java 层对 FlutterImageView 的 resize 造成创建 ImageReader 的宽高为 0 了,Android 不允许创建宽高是 0 的 ImageReader:
所以又有一个 bugfix 的提交,已 merge 到官方 😂
https://github.com/flutter/engine/pull/27946
最终的 Pull Request
已合并到官方 Flutter 引擎: https://github.com/flutter/engine/pull/27662
给官方贡献代码的小经验
如果没有 issue,最好创建一个 issue,然后自己提 Pull Request 解决自己的 issue😂
最好包含 test,即使改了一行代码,也是可以写 test 的,而且他们一看到 test 就很放心,也能更好的让后面的人理解你的代码的意图,否则有一个机器人会说你没 test,并且打上标签:
现在最新代码在 git push 的时候,会通过 git_hooks 自动检查所有类型的源码(包括 iOS/Android/c++/gn 等等)的格式和规范,有不符合规范的直接生成一个 diff,作为修改建议。
这个还可以帮我们自动触发然后自动修改,命令是:
dart ci/bin/format.dart -f
其中-f 是让它自动 fix
官方 review 代码还是很严格的,比如对函数语义的修改,需要同步对 docstring 进行修改;又比如一言不合给你扔一个 c++规范;或者代码进行重复的 map[key] = value 和 map[key] 的取值,可以用 iterator 代替;auto 关键词不能滥用,lambda 需要指定返回类型,等等
总结
作为 Flutter Infra 团队的开发,在和我们的业务部门去实践 Flutter 2.0 轻量级引擎和卡片方案的落地的过程中,我们团队做了很多性能和稳定性的优化,包括空安全迁移、Image Cache 共享、文本动态对齐、Platform View 的多引擎支持、过渡动画性能优化、大内存优化、官方 issue 和稳定性修复等很多工作。
我们在努力支持字节内部业务的同时,也会持续将其中比较通用的一些 fix 和优化方案的 Pull Request 提交给官方,和全世界开发者一起共建更好的 Flutter 社区。
另外,我们也通过字节跳动的企业级技术服务平台火山引擎对外部客户提供 Flutter 全流程解决方案,助力使用 Flutter 技术栈的开发团队高效稳健地落地业务。
关于字节终端技术团队
字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、瓜瓜龙等,在移动端、Web、Desktop 等各终端都有深入研究。
就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣请联系 chenxuwei.cxw@bytedance.com,邮件主题 简历-姓名-求职意向-期望城市-电话。
评论