天天看點

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

作者:位元組移動技術-李皓骅

本文介紹了 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 消費兩個隊列的消息的效果),見如下的示意圖,這樣我們可以有個初步的印象:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

首先,介紹下 PlatformView 是什麼,其實它簡單了解成——平台相關的 View 。也就是說,在Android 和 iOS 平台原生有這樣的控件,但是在Flutter的跨平台控件庫裡沒有實作過的一些Widget,這些控件我們可以使用Flutter提供的PlatformView的機制,來做一個渲染和橋接,并且在上層可以用Flutter的方法去建立、控制這些原生View,來保證兩端跨平台接口統一。

比如WebView,地圖控件,第三方廣告SDK等等這些場景,我們就必須要用到PlatformView了。

舉一個例子,下圖就是 Android 上使用 PlatformView 機制的 WebView 控件和 Flutter控件的混合渲染的效果:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

可以看到Android ViewTree上确實存在一個WebView。

下面是一個Flutter的使用WebView的上層代碼示例:

黃色背景内容是使用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 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 以及線程合并解決方案摘要背景介紹線程合并解決方案

關于線程合并,我們可能有下面幾個疑問:

為什麼不用 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相關的代碼也可以看到合并的操作:

<code>``c++&lt;br/&gt;// src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc&lt;br/&gt;// |ExternalViewEmbedder|&lt;br/&gt;PostPrerollResult AndroidExternalViewEmbedder::PostPrerollAction(&lt;br/&gt;fml::RefPtr&amp;lt;fml::RasterThreadMerger&amp;gt; raster_thread_merger) {&lt;br/&gt;if (!FrameHasPlatformLayers()) {&lt;br/&gt;// 這裡判斷目前frame有沒有platform view,有就直接傳回&lt;br/&gt;return PostPrerollResult::kSuccess;&lt;br/&gt;}&lt;br/&gt;if (!raster_thread_merger-&amp;gt;IsMerged()) { &lt;br/&gt;// 如果有platform view并且沒merger,就進行merge操作&lt;br/&gt;// The raster thread merger may be disabled if the rasterizer is being&lt;br/&gt;// created or teared down.&lt;br/&gt;//&lt;br/&gt;// In such cases, the current frame is dropped, and a new frame is attempted&lt;br/&gt;// with the same layer tree.&lt;br/&gt;//&lt;br/&gt;// Eventually, the frame is submitted once this method returns</code>kSuccess`.

// At that point, the raster tasks are handled on the platform thread.

raster_thread_merger-&gt;MergeWithLease(kDefaultMergedLeaseDuration);

CancelFrame();

return PostPrerollResult::kSkipAndRetryFrame;

}

// 擴充并更新租約,使得後面沒有platform view并且租約計數器降低到0的時候,開始unmerge操作

raster_thread_merger-&gt;ExtendLeaseTo(kDefaultMergedLeaseDuration);

// Surface switch requires to resubmit the frame.

// TODO(egarciad): https://github.com/flutter/flutter/issues/65652

if (previous_frame_viewcount == 0) {

return PostPrerollResult::kResubmitFrame;

return PostPrerollResult::kSuccess;

取下一個任務的<code>PeekNextTaskUnlocked</code>的邏輯(參考注釋):

```c++

// src/flutter/fml/message_loop_task_queues.cc

const DelayedTask&amp; MessageLoopTaskQueues::PeekNextTaskUnlocked(

TaskQueueId owner,

TaskQueueId&amp; top_queue_id) const {

FML_DCHECK(HasPendingTasksUnlocked(owner));

const auto&amp; entry = queueentries.at(owner);

const TaskQueueId subsumed = entry-&gt;owner_of;

if (subsumed == _kUnmerged) { // 如果沒merge的話,就取自己目前的top任務

top_queue_id = owner;

return entry-&gt;delayed_tasks.top();

const auto&amp; owner_tasks = entry-&gt;delayed_tasks;

const auto&amp; subsumed_tasks = queueentries.at(subsumed)-&gt;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 &amp;&amp; subsumed_has_task) {

const auto owner_task = owner_tasks.top();

const auto subsumed_task = subsumed_tasks.top();

// 如果merge了的話,根據标記判斷,就取兩個隊列的top任務,再比較誰比較靠前

if (owner_task &gt; subsumed_task) {

top_queue_id = subsumed;

} else {

} else if (owner_has_task) {

return queueentries.at(top_queue_id)-&gt;delayed_tasks.top();

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有一點差別:

問題1是Flutter 1.22+獨立引擎的問題,我在代碼中搜尋<code>raster_thread_merger.cc(48)] Check failed: success. Unable to merge the raster and platform threads</code>其中raster_thread_merger.cc的48行這樣的代碼:

當<code>success == false</code>的時候會觸發SIGABRT,看Merge()函數什麼時候傳回false:

bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {

if (owner == subsumed) {

return true;

std::mutex&amp; owner_mutex = GetMutex(owner);

std::mutex&amp; subsumed_mutex = GetMutex(subsumed);

std::scoped_lock lock(owner_mutex, subsumed_mutex);

auto&amp; owner_entry = queueentries.at(owner);

auto&amp; subsumed_entry = queueentries.at(subsumed);

if (owner_entry-&gt;owner_of == subsumed) {

std::vector&lt;TaskQueueId&gt; owner_subsumed_keys = {

owner_entry-&gt;owner_of, owner_entry-&gt;subsumed_by, subsumed_entry-&gt;owner_of,

subsumed_entry-&gt;subsumed_by};

for (auto key : owner_subsumed_keys) {

if (key != _kUnmerged) {

return false; // &lt;--- 這裡是傳回false唯一的可能

owner_entry-&gt;owner_of = subsumed;

subsumed_entry-&gt;subsumed_by = owner;

if (HasPendingTasksUnlocked(owner)) {

WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner));

可以看到Merge調用了兩次,并且第二次調用的第0個元素是2,印證了上面for循環出現不等于unmerge常量的情況了。

其中的2和5分别是引擎1和引擎2的raster線程,通過

再 adb pull /data/anr/trace_00 拉出來看真實的線程也可以看到<code>1.ui, 2.ui, 1.raster, 2.raster, 1.io, 2.io</code>這樣的被設定了名字線程(有pthread_setname之類的函數):

在Google搜尋這個<code>Unable to merge the raster and platform threads</code>在也可以搜到一個送出:

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的時候,設定了<code>block_merging</code>标記,第二次以及後面的merge操作會失敗并列印一個日志:

是以,在官方那是一個todo,是待實作的feature。

問題2是Flutter 2.0+輕量級引擎下的問題,直接看輕量級多引擎下,檢查失敗的那一行的源碼:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

很明顯,和上面的獨立多引擎不同,這裡在建立RasterThreadMerger的構造函數的FML_CHECK檢查就失敗了,證明platform和raster已經是merge的狀态了,是以這裡也是SIGABRT并且程式退出了。

通過列印log看到兩個引擎的platform和raster的id是共享的,引擎1和引擎2的platform_queue_id都是0,raster_queue_id都是2

很容易我們可以推理得到,多引擎的每個引擎都需要有一套四大線程,它們可以選擇公用,或者也可以選擇建立自己獨立的線程。

我們通過之前的log列印的task_queue_id,分析一下兩個問題唯一的差別:

在問題1(兩個獨立引擎中)的情況是這樣的(四大線程除了platform,其他三個線程不共享):

獨立引擎1

獨立引擎2

platform_task_queue_id

ui_task_queue_id

1

4

raster_task_queue_id

2

5

io_task_queue_id

3

6

在問題2(兩個輕量級引擎中)的情況是這樣的(四大線程全部共享):

輕量級引擎1

輕量級引擎2

是以相對來講,感覺問題2更容易解決,并且我們使用flutter 2.0和卡片方案的業務,馬上就将要遇到這個問題。

官方的輕量級引擎有一個TODO清單,把這個問題标記成Cleanup的任務:

https://github.com/flutter/flutter/issues/72009

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

官方标記了P5優先級:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

因為業務需要是以直接就不等了,我們幹脆自己實作它。

既然在輕量級引擎下,platform 線程和 raster 線程都是共享的,隻是 engine 和 rasterizer 的對象是分開的,并且現在的邏輯是分别在兩個引擎裡,new 了自己的 RasterThreadMerger對象,進行後續的 merge 和 unmerge 操作。并且在 merge 的時候做是否Owns的檢查。

那我們可以簡單的做這幾件事:

改成去掉 Owns() 的檢查和相關線程檢查

共享一個 RasterThreadMerger 對象進行 merge 和 unmerge 操作

先不管那個 lease_term (租約)計數器,留下後續處理

修改方案基本是坤神(我們Flutter組的戰友)的 prototype 送出的方案,并且加一些邊角的處理即可。

Prototype原型的關鍵修改的地方:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

每個帶title的都是一個FlutterView,終于不崩潰了:

效果截圖:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

但是這隻是一個原型,很多狀态問題和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的情況,然後貢獻給官方。

經過檢視代碼裡<code>raster_thread_merger</code>對象是<code>rasterizer</code>的一個成員:

以下都是 RasterThreadMerger 類裡的成員函數,都是需要我們修改成一對多merge以後,也保證去維護正常調用時機的API:

merger建立的時候,需要考慮某些情況下不支援merger需要保持merger不被建立出來(比如某些不支援的平台或者某些unittest):

那麼我們有一種選擇是在每個engine各自的rasterizer的建立的時候,改改它的邏輯,在raster_queue_id相同的時候,複用之前的對象,聽起來是個好辦法。

畫了個圖作為兩種情況的展示:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

關于線程什麼情況下允許合并,什麼情況下不允許合并的示意圖:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

另外還有一種情況沒有列出,自己merge到自己的情況:現在的代碼預設傳回true的。

總結一句話就是一個queue可以合并多個queue(可以有多個下遊),但是一個queue不可以有多個上遊。

此實作的設計:

首先最重要的:将TaskQueueEntry中的成員<code>owner_of</code>從<code>TaskQueueId</code>改成<code>std::set&amp;lt;TaskQueueId&amp;gt; owner_of</code>,來記錄這個線程所有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内的方法,然後用一個<code>std::map&amp;lt;ThreadMergerCaller, int&amp;gt;</code>來記錄合并狀态和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&lt;TaskQueueId&gt; owner_of; // 原來是TaskQueueId owner_of;

merge和unmerge相關的檢查(省略,詳情可以參考 Pull Request中代碼送出)

和官方一樣,使用FlutterFragment的方式來嵌入多引擎的時候,FlutterSurfaceView會給surface設定ZOrder,這時候多個Surface會有ZOrder争搶top的問題

需要在建立的時候,去掉Transparent的flag,需要這樣改:(這個問題被坑了很久,差點沒讓我放棄這個送出)

在iOS做unittest的時候,發現有相應的崩潰,也是沒有崩潰的stack和詳細log,後來發現iOS目錄下有一個README,提到了使用xcode可以打開unittest工程,開啟模拟器自動測試,并且發現可以直接在我沒有attach的情況下,自動attach lldb并且定位到崩潰的那一行代碼:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

官方review代碼的時候,提出的最大問題是之前用了map做一個全局static的<code>std::map&amp;lt;Pair&amp;lt;QueueId, QueueId&amp;gt;, SharedThreadMerger&amp;gt;</code>的字典static變量,用來取platform&amp;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平台上的失敗資訊:

Flutter 多引擎支援 PlatformView 以及線程合并解決方案摘要背景介紹線程合并解決方案

可以看到什麼log都沒有。

困擾半天最終決定:裝一個windows虛拟機!神奇的事情發生了,在我的windows 10 + flutter engine環境下編譯然後運作我的test,結果全都過了。驚愕!最終還是通過兩分法看修改,定位到了一個 unittest 的抽取的改法造成了問題。

留個題目:可以看出如下代碼為什麼windows會有問題嗎?

/// A mock task queue NOT calling MessageLoop-&gt;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 = &amp;fml::MessageLoop::GetCurrent();

latch.Signal();

term.Wait();

}) {

latch.Wait();

// .. 省略析構函數, term.Signal() 和 thread.join() 等等

};

繼續閱讀