关于跨平台:Flutter-多引擎支持-PlatformView-以及线程合并解决方案

17次阅读

共计 19861 个字符,预计需要花费 50 分钟才能阅读完成。

作者:字节挪动技术 - 李皓骅

摘要

本文介绍了 Flutter 多引擎下,应用 PlatformView 场景时不能绕开的一个线程合并问题,以及它最终的解决方案。最终 Pull Request 曾经 merge 到 Google 官网 Flutter 仓库:

https://github.com/flutter/en…

本文关键点:

  1. 线程合并,实际上指的并不是操作系统有什么高级接口,能够把两个 pthread 合起来,而是 flutter 引擎中的四大 Task Runner 里,用一个 Task Runner 同时生产解决两个 Task Queue 中排队的工作。
  2. 线程合并问题,指的是 Flutter 引擎四大线程(Platform 线程、UI 线程、Raster 线程、IO 线程)其中的 Platform 线程和 Raster 线程在应用 PlatformView 的场景时须要合并和拆散的问题。之前的官网的线程合并机制,只反对一对一的线程合并,但多引擎场景就须要一对多的合并和一些相干的配套逻辑。具体请看下文介绍。
  3. 对于 Flutter 引擎的四大 Task Runner 能够参考官网 wiki 中的 Flutter Engine 线程模型:https://github.com/flutter/fl…
  4. 本文介绍的线程合并操作(也就实现了一个 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 线程各司其职:

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 拜访的耗时过程的专用线程,例如解码图像文件。

如下图所示:

线程合并

对于线程合并,咱们可能有上面几个疑难:

  1. 为什么不必 platform view 的时候,两种多引擎工作的好好的?
  2. 为什么应用 platform view 的时候,iOS 和 Android 两端,都须要 merge 么,能不能不 merge?
  3. merge 当前,在不应用 platform view 的 flutter 页面里,还会勾销 merge 还原回来么?

咱们来怀着这几个疑难去剖析问题。

为什么要线程合并?

为什么在应用 PlatformView 的时候,须要把 Platform 线程和 Raster 线程合并起来?

简略的说就是:

  1. 所有 PlatformView 的操作须要在主线程里进行(Platform 线程指的就是 App 的主线程),否则在 Raster 线程解决 PlatformView 的 composition 和绘制等操作时,Android Framework 查看到非 App 主线程,会间接抛异样;
  2. Flutter 的 Raster 渲染操作和 PlatformView 的渲染逻辑是各自渲染的,当他们一起应用的时候每一帧渲染时候,须要做同步,而比较简单间接的一种实现形式就是把两个工作队列合并起来,只让一个主线程的 runner 去一一生产两个队列的工作;
  3. 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,咱们剖析下各自的四大线程的运行状态:

  1. 首先没有 PlatformView 的时候的状况下,四大 Task Runner 的状态:

Platform ✅ / UI ✅ / Raster ✅ / IO ✅

  1. 应用 PlatformView 的时候的状况下,四大 Task Runner 的状态:

Platform ✅(同时解决 Raster 线程的工作队列) / UI ✅ / Raster ❌(闲置) / IO ✅

merge 和 unmerge 操作,能够如下图所示:

一个 runner 如何生产两个工作队列?

要害的两个点就是:

  1. TaskQueueEntry 类中有两个成员变量,记录了以后队列的上游和上游的 queue_id
  2. 在 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/fl…

也就是说,在独立的多引擎下,应用 platform view 的时候,会因为 raster_thread_merger 不反对多于一对一的合并(merge)操作而失败并报错。

解体的 demo:

https://github.com/eggfly/una…

看日志这是一个解体,而后接一个 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/en…

咱们在用官网的 lightweight multiple engine 的 sample 代码的时候,尝试在多引擎下加上 PlatformView,也就是在 main.dart 里加上 webview。

官网 demo 代码:https://github.com/flutter/sa…

运行起来会有这样的解体日志,这里的谬误和问题 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/en…

提交介绍说:

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. 在问题 1(两个独立引擎中)的状况是这样的(四大线程除了 platform,其余三个线程不共享):
独立引擎 1 独立引擎 2
platform_task_queue_id 0 0
ui_task_queue_id 1 4
raster_task_queue_id 2 5
io_task_queue_id 3 6
  1. 在问题 2(两个轻量级引擎中)的状况是这样的(四大线程全副共享):
轻量级引擎 1 轻量级引擎 2
platform_task_queue_id 0 0
ui_task_queue_id 1 1
raster_task_queue_id 2 2
io_task_queue_id 3 3

所以绝对来讲,感觉问题 2 更容易解决,并且咱们应用 flutter 2.0 和卡片计划的业务,马上就将要遇到这个问题。

官网的轻量级引擎有一个 TODO 列表,把这个问题标记成 Cleanup 的工作:

https://github.com/flutter/fl…

官网标记了 P5 优先级:

因为业务须要所以间接就不等了,咱们罗唆本人实现它。

线程合并解决方案

疾速解决问题 2:解决轻量级引擎的问题

既然在轻量级引擎下,platform 线程和 raster 线程都是共享的,只是 engine 和 rasterizer 的对象是离开的,并且当初的逻辑是别离在两个引擎里,new 了本人的 RasterThreadMerger 对象,进行后续的 merge 和 unmerge 操作。并且在 merge 的时候做是否 Owns 的查看。

那咱们能够简略的做这几件事:

  1. 改成去掉 Owns() 的检查和相干线程查看
  2. 共享一个 RasterThreadMerger 对象进行 merge 和 unmerge 操作
  3. 先不论那个 lease_term (租约)计数器,留下后续解决

批改计划根本是坤神(咱们 Flutter 组的战友)的 prototype 提交的计划,并且加一些边角的解决即可。

Prototype 原型的要害批改的中央:

每个带 title 的都是一个 FlutterView,终于不解体了:

成果截图:

然而这只是一个原型,很多状态问题和 merge 的逻辑咱们没有解决的很好,问题包含:

  1. 咱们不能像原型一样,在字节的 Flutter 引擎里间接 hardcode 写死共享一个 merger 对象,所以 2.0 之前的独立多引擎仍旧会有问题
  2. 咱们没正确处理 IsMerged 函数的正确返回后果
  3. 咱们还没有正确处理 lease_term 的计数器,lease_term 计数器降到 0 的时候,应该 unmerge
  4. 咱们假象有一种 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_ofTaskQueueId改成std::set<TaskQueueId> owner_of,来记录这个线程所有 merge 过的 subsumed_id(一对多的 merge 关系)
  • 代码的批改平台独立的,能够让 Android 和 iOS 共享雷同的代码逻辑,确保不同平台的相干目录中的代码没有被更改(之前做过一个版本的计划,是 Android、iOS 别离批改了 embedder 类的逻辑)
  • 删除了之前官网禁用阻塞逻辑的代码(也就是 revert 了官网之前的这个提交:https://github.com/flutter/en…)
  • 为了缩小现有代码的更改数量,把旧的 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(之前被一个官网提交禁用了,发现改什么代码都不起作用,比拟坑)

解决方案相干的代码

  1. 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;
  1. 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();}
  1. merge 和 unmerge 相干的查看(省略,详情能够参考 Pull Request 中代码提交)

实现过程中的小坑

  1. 和官网一样,应用 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>()
  1. 在 iOS 做 unittest 的时候,发现有相应的解体,也是没有解体的 stack 和具体 log,起初发现 iOS 目录下有一个 README,提到了应用 xcode 能够关上 unittest 工程,开启模拟器自动测试,并且发现能够间接在我没有 attach 的状况下,主动 attach lldb 并且定位到解体的那一行代码:

  1. 官网 review 代码的时候,提出的最大问题是之前用了 map 做一个全局 static 的 std::map<Pair<QueueId, QueueId>, SharedThreadMerger> 的字典 static 变量,用来取 platform&raster 这一个 pair 的 merger,然而老外扔给我一个 google c++ 标准,明确写了 non-trivial 的类型才容许保留为全局变量,官网标准文档:https://google.github.io/styl…

最终通过把 merger 作为 Shell 类的成员变量来解决这个生命周期的问题。

  1. 在测试的时候发现 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() 等等
};

  1. 跑起来两个 webview 的 demo 当前,点上面的键盘,会有一个 crash(上面的界面弹出键盘当前就崩了):

后果是 java 层对 FlutterImageView 的 resize 造成创立 ImageReader 的宽高为 0 了,Android 不容许创立宽高是 0 的 ImageReader:

所以又有一个 bugfix 的提交,已 merge 到官网 😂

https://github.com/flutter/en…

最终的 Pull Request

已合并到官网 Flutter 引擎: https://github.com/flutter/en…

给官网奉献代码的小教训

  1. 如果没有 issue,最好创立一个 issue,而后本人提 Pull Request 解决本人的 issue😂
  2. 最好蕴含 test,即便改了一行代码,也是能够写 test 的,而且他们一看到 test 就很释怀,也能更好的让前面的人了解你的代码的用意,否则有一个机器人会说你没 test,并且打上标签:

  1. 当初最新代码在 git push 的时候,会通过 git_hooks 主动查看所有类型的源码(包含 iOS/Android/c++/gn 等等)的格局和标准,有不符合规范的间接生成一个 diff,作为批改倡议。

这个还能够帮咱们主动触发而后主动批改,命令是:

dart ci/bin/format.dart -f 其中 - f 是让它主动 fix

  1. 官网 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,邮件主题 简历 - 姓名 - 求职意向 - 冀望城市 - 电话

正文完
 0