Flutter 多引擎支持 PlatformView 以及线程合并解决方案

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

摘要

本文介绍了 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 消费两个队列的消息的效果),见如下的示意图,这样我们可以有个初步的印象:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第1张图片

背景介绍

什么是 PlatformView?

首先,介绍下 PlatformView 是什么,其实它简单理解成——平台相关的 View 。也就是说,在Android 和 iOS 平台原生有这样的控件,但是在Flutter的跨平台控件库里没有实现过的一些Widget,这些控件我们可以使用Flutter提供的PlatformView的机制,来做一个渲染和桥接,并且在上层可以用Flutter的方法去创建、控制这些原生View,来保证两端跨平台接口统一。

比如WebView,地图控件,第三方广告SDK等等这些场景,我们就必须要用到PlatformView了。

举一个例子,下图就是 Android 上使用 PlatformView 机制的 WebView 控件和 Flutter控件的混合渲染的效果:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第2张图片

可以看到Android ViewTree上确实存在一个WebView。

下面是一个Flutter的使用WebView的上层代码示例:

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

// .. 省略App代码
class _BodyState extends State {
  @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 访问的耗时过程的专用线程,例如解码图像文件。

如下图所示:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第3张图片

线程合并

关于线程合并,我们可能有下面几个疑问:

  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的所有任务,参考下面的示意图:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第4张图片

我们分析external_view_embedder.cc相关的代码也可以看到合并的操作:

// src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc
// |ExternalViewEmbedder|
PostPrerollResult AndroidExternalViewEmbedder::PostPrerollAction(
    fml::RefPtr 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 操作,可以如下图所示:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第5张图片

一个 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 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行这样的代码:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第6张图片

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 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可以看出:

img

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之类的函数):

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第7张图片

在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操作会失败并打印一个日志:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第8张图片

所以,在官方那是一个todo,是待实现的feature。

分析2:轻量级多引擎线程合并问题

问题2是Flutter 2.0+轻量级引擎下的问题,直接看轻量级多引擎下,检查失败的那一行的源码:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第9张图片

很明显,和上面的独立多引擎不同,这里在创建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...

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第10张图片

官方标记了P5优先级:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第11张图片

因为业务需要所以直接就不等了,我们干脆自己实现它。

线程合并解决方案

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

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

那我们可以简单的做这几件事:

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

修改方案基本是坤神(我们Flutter组的战友)的 prototype 提交的方案,并且加一些边角的处理即可。

Prototype原型的关键修改的地方:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第12张图片

每个带title的都是一个FlutterView,终于不崩溃了:

效果截图:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第13张图片

但是这只是一个原型,很多状态问题和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 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 {
 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) {
  // ... 省略
  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相同的时候,复用之前的对象,听起来是个好办法。

实现的方案

画了个图作为两种情况的展示:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第14张图片

关于线程什么情况下允许合并,什么情况下不允许合并的示意图:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第15张图片

另外还有一种情况没有列出,自己merge到自己的情况:现在的代码默认返回true的。

总结一句话就是一个queue可以合并多个queue(可以有多个下游),但是一个queue不可以有多个上游。

此实现的设计:

  • 首先最重要的:将TaskQueueEntry中的成员owner_ofTaskQueueId改成std::set 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来记录合并状态和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 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 top_task;
  
  // 更新当前最小的任务的lambda函数
  std::function 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()
  1. 在iOS做unittest的时候,发现有相应的崩溃,也是没有崩溃的stack和详细log,后来发现iOS目录下有一个README,提到了使用xcode可以打开unittest工程,开启模拟器自动测试,并且发现可以直接在我没有attach的情况下,自动attach lldb并且定位到崩溃的那一行代码:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第16张图片

  1. 官方review代码的时候,提出的最大问题是之前用了map做一个全局static的std::map, 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平台上的失败信息:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第17张图片

可以看到什么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() 等等
};

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第18张图片

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

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第19张图片

结果是java层对FlutterImageView的resize造成创建ImageReader的宽高为0了,Android不允许创建宽高是0的ImageReader:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第20张图片

所以又有一个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,并且打上标签:

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第21张图片

  1. 现在最新代码在 git push 的时候,会通过 git_hooks 自动检查所有类型的源码(包括iOS/Android/c++/gn等等)的格式和规范,有不符合规范的直接生成一个diff,作为修改建议。

这个还可以帮我们自动触发然后自动修改,命令是:

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

Flutter 多引擎支持 PlatformView 以及线程合并解决方案_第22张图片

  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等各终端都有深入研究。

就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣请联系 [email protected],邮件主题 简历-姓名-求职意向-期望城市-电话

你可能感兴趣的:(跨平台)