閑魚技術-福居
本文結合Flutter Engine官方文檔讨論了Flutter Engine内的線程管理模式以及Dart Isolate機制。
Flutter 是什麼?
Flutter簡介
Flutter是Google主導開發的高品質高性能移動跨平台UI開發套件。使用Flutter你可以使用Dart語言高效快速開發高品質的跨平台App,同時Flutter還可以可以與現存的Native代碼相容。目前在世界範圍内被衆多開發者群組織使用,而且它是開源免費的!
Flutter優勢與前景
目前Flutter拿來比較最多的是Reactive Native,實際上Flutter跟RN有本質的差別。Flutter UI渲染是自己實作,這跟RN JS Bridge的形式有差別。這也是Flutter性能的一個突破點。使用Flutter用開發效率高,運作效率高,UI靈活性擴充性高等特點。相對于JS Bridge擴充型的跨平台實作,Flutter有着更加廣闊的想象空間。
更加詳細的介紹可以浏覽此連結:
Flutter IOFlutter 線程管理簡述
Flutter Engine自己不建立管理線程。Flutter Engine線程的建立和管理是由embedder負責的。
注意:Embeder是指将引擎移植的平台的中間層代碼。
Flutter Engine要求Embeder提供四個Task Runner。盡管Flutter Engine不在乎Runner具體跑在哪個線程,但是它需要線程配置在整一個生命周期裡面保持穩定。也就是說一個Runner最好始終保持在同一線程運作。這四個主要的Task Runner包括:
Platform Task Runner
Flutter Engine的主Task Runner,運作Platform Task Runner的線程可以了解為是主線程。類似于Android Main Thread或者iOS的Main Thread。但是我們要注意Platform Task Runner和iOS之類的主線程還是有差別的。
對于Flutter Engine來說Platform Runner所在的線程跟其它線程并沒有實質上的差別,隻不過我們人為賦予它特定的含義便于了解區分。實際上我們可以同時啟動多個Engine執行個體,每個Engine對應一個Platform Runner,每個Runner跑在各自的線程裡。這也是Fuchsia(Google正在開發的操作引擎)裡Content Handler的工作原理。一般來說,一個Flutter應用啟動的時候會建立一個Engine執行個體,Engine建立的時候會建立一個線程供Platform Runner使用。
跟Flutter Engine的所有互動(接口調用)必須發生在Platform Thread,試圖在其它線程中調用Flutter Engine會導緻無法預期的異常。這跟iOS UI相關的操作都必須在主線程進行相類似。需要注意的是在Flutter Engine中有很多子產品都是非線程安全的。一旦引擎正常啟動運作起來,所有引擎API調用都将在Platform Thread裡發生。
Platform Runner所在的Thread不僅僅處理與Engine互動,它還處理來自平台的消息。這樣的處理比較友善的,因為幾乎所有引擎的調用都隻有在Platform Thread進行才能是安全的,Native Plugins不必要做額外的線程操作就可以保證操作能夠在Platform Thread進行。如果Plugin自己啟動了額外的線程,那麼它需要負責将傳回結果派發回Platform Thread以便Dart能夠安全地處理。規則很簡單,對于Flutter Engine的接口調用都需保證在Platform Thread進行。
需要注意的是,阻塞Platform Thread不會直接導緻Flutter應用的卡頓(跟iOS android主線程不同)。盡管如此,平台對Platform Thread還是有強制執行限制。是以建議複雜計算邏輯操作不要放在Platform Thread而是放在其它線程(不包括我們現在讨論的這個四個線程)。其他線程處理完畢後将結果轉發回Platform Thread。長時間卡住Platform Thread應用有可能會被系統Watchdot強行殺死。
UI Task Runner Thread(Dart Runner)
UI Task Runner被Flutter Engine用于執行Dart root isolate代碼(isolate我們後面會講到,姑且先簡單了解為Dart VM裡面的線程)。Root isolate比較特殊,它綁定了不少Flutter需要的函數方法。Root isolate運作應用的main code。引擎啟動的時候為其增加了必要的綁定,使其具備排程送出渲染幀的能力。對于每一幀,引擎要做的事情有:
- Root isolate通知Flutter Engine有幀需要渲染。
- Flutter Engine通知平台,需要在下一個vsync的時候得到通知。
- 平台等待下一個vsync
- 對建立的對象和Widgets進行Layout并生成一個Layer Tree,這個Tree馬上被送出給Flutter Engine。目前階段沒有進行任何光栅化,這個步驟僅是生成了對需要繪制内容的描述。
- 建立或者更新Tree,這個Tree包含了用于螢幕上顯示Widgets的語義資訊。這個東西主要用于平台相關的輔助Accessibility元素的配置和渲染。
除了渲染相關邏輯之外Root Isolate還是處理來自Native Plugins的消息響應,Timers,Microtasks和異步IO。
我們看到Root Isolate負責建立管理的Layer Tree最終決定什麼内容要繪制到螢幕上。是以這個線程的過載會直接導緻卡頓掉幀。
如果确實有無法避免的繁重計算,建議将其放到獨立的Isolate去執行,比如使用compute關鍵字或者放到非Root Isolate,這樣可以避免應用UI卡頓。但是需要注意的是非Root Isolate缺少Flutter引擎需要的一些函數綁定,你無法在這個Isolate直接與Flutter Engine互動。是以隻在需要大量計算的時候采用獨立Isolate。
GPU Task Runner
GPU Task Runner被用于執行裝置GPU的相關調用。UI Task Runner建立的Layer Tree資訊是平台不相關,也就是說Layer Tree提供了繪制所需要的資訊,具體如何實作繪制取決于具體平台和方式,可以是OpenGL,Vulkan,軟體繪制或者其他Skia配置的繪圖實作。GPU Task Runner中的子產品負責将Layer Tree提供的資訊轉化為實際的GPU指令。GPU Task Runner同時也負責配置管理每一幀繪制所需要的GPU資源,這包括平台Framebuffer的建立,Surface生命周期管理,保證Texture和Buffers在繪制的時候是可用的。
基于Layer Tree的處理時長和GPU幀顯示到螢幕的耗時,GPU Task Runner可能會延遲下一幀在UI Task Runner的排程。一般來說UI Runner和GPU Runner跑在不同的線程。存在這種可能,UI Runner在已經準備好了下一幀的情況下,GPU Runner卻還正在向GPU送出上一幀。這種延遲排程機制確定不讓UI Runner配置設定過多的任務給GPU Runner。
前面我們提到GPU Runner可以導緻UI Runner的幀排程的延遲,GPU Runner的過載會導緻Flutter應用的卡頓。一般來說使用者沒有機會向GPU Runner直接送出任務,因為平台和Dart代碼都無法跑進GPU Runner。但是Embeder還是可以向GPU Runner送出任務的。是以建議為每一個Engine執行個體都建立一個專用的GPU Runner線程。
IO Task Runner
前面讨論的幾個Runner對于執行任務的類型都有比較強的限制。Platform Runner過載可能導緻系統WatchDog強殺,UI和GPU Runner過載則可能導緻Flutter應用的卡頓。但是GPU線程有一些必要操作是比較耗時間的,比如IO,而這些操作正是IO Runner需要處理的。
IO Runner的主要功能是從圖檔存儲(比如磁盤)中讀取壓縮的圖檔格式,将圖檔資料進行處理為GPU Runner的渲染做好準備。在Texture的準備過程中,IO Runner首先要讀取壓縮的圖檔二進制資料(比如PNG,JPEG),将其解壓轉換成GPU能夠處理的格式然後将資料上傳到GPU。這些複雜操作如果跑在GPU線程的話會導緻Flutter應用UI卡頓。但是隻有GPU Runner能夠通路GPU,是以IO Runner子產品在引擎啟動的時候配置了一個特殊的Context,這個Context跟GPU Runner使用的Context在同一個ShareGroup。事實上圖檔資料的讀取和解壓是可以放到一個線程池裡面去做的,但是這個Context的通路隻能在特定線程才能保證安全。這也是為什麼需要有一個專門的Runner來處理IO任務的原因。擷取諸如
ui.Image
這樣的資源隻有通過async call,當這個調用發生的時候Flutter Framework告訴IO Runner進行剛剛提到的那些圖檔異步操作。這樣GPU Runner可以使用IO Runner準備好的圖檔資料而不用進行額外的操作。
使用者操作,無論是Dart Code還是Native Plugins都是沒有辦法直接通路IO Runner。盡管Embeder可以将一些一般複雜任務排程到IO Runner,這不會直接導緻Flutter應用卡頓,但是可能會導緻圖檔和其它一些資源加載的延遲間接影響性能。是以建議為IO Runner建立一個專用的線程。
各個平台目前預設Runner線程實作
前面我們提到Engine Runner的線程可以按照實際情況進行配置,各個平台目前有自己的實作政策。
iOS和Android
Mobile平台上面每一個Engine執行個體啟動的時候會為UI,GPU,IO Runner各自建立一個新的線程。所有Engine執行個體共享同一個Platform Runner和線程。
Fuchsia
每一個Engine執行個體都為UI,GPU,IO,Platform Runner建立各自新的線程。
自定義配置線程可行方案
我們注意到Mobile平台上面,Platform Runner和Thread是共享的。
引擎源碼如下:
Shell::Shell(fxl::CommandLine command_line)
: command_line_(std::move(command_line)) {
FXL_DCHECK(!g_shell);
gpu_thread_.reset(new fml::Thread("gpu_thread"));
ui_thread_.reset(new fml::Thread("ui_thread"));
io_thread_.reset(new fml::Thread("io_thread"));
// Since we are not using fml::Thread, we need to initialize the message loop
// manually.
fml::MessageLoop::EnsureInitializedForCurrentThread();
blink::Threads threads(fml::MessageLoop::GetCurrent().GetTaskRunner(),
gpu_thread_->GetTaskRunner(),
ui_thread_->GetTaskRunner(),
io_thread_->GetTaskRunner());
blink::Threads::Set(threads);
blink::Threads::Gpu()->PostTask([this]() { InitGpuThread(); });
blink::Threads::UI()->PostTask([this]() { InitUIThread(); });
blink::SetRegisterNativeServiceProtocolExtensionHook(
PlatformViewServiceProtocol::RegisterHook);
}
這裡我們可以進行改動,讓引擎每個執行個體初始化獨自的線程:
gpu_thread_.reset(new fml::Thread("gpu_thread"));
ui_thread_.reset(new fml::Thread("ui_thread"));
io_thread_.reset(new fml::Thread("io_thread"));
platform_thread_.reset(new fml::Thread("platform_thread"));
blink::Threads threads(platform_thread_->GetTaskRunner(),
gpu_thread_->GetTaskRunner(),
ui_thread_->GetTaskRunner(),
io_thread_->GetTaskRunner());
理論上你可以配置任意線程供其使用,不過最好遵循最佳實踐。
具體代碼導讀
iOS Android平台可以參考Flutter Engine源碼:
flutter/common/threads.cc
flutter/shell/common/shell.cc
Dart isolate機制
An isolated Dart execution context. 這是文檔對isolate的定義。
isolate定義
isolate是Dart對actor并發模式的實作。運作中的Dart程式由一個或多個actor組成,這些actor也就是Dart概念裡面的isolate。isolate是有自己的記憶體和單線程控制的運作實體。isolate本身的意思是“隔離”,因為isolate之間的記憶體在邏輯上是隔離的。isolate中的代碼是按順序執行的,任何Dart程式的并發都是運作多個isolate的結果。因為Dart沒有共享記憶體的并發,沒有競争的可能性是以不需要鎖,也就不用擔心死鎖的問題。
isolate之間的通信
由于isolate之間沒有共享記憶體,是以他們之間的通信唯一方式隻能是通過Port進行,而且Dart中的消息傳遞總是異步的。
isolate與普通線程的差別
我們可以看到isolate神似Thread,但實際上兩者有本質的差別。作業系統内内的線程之間是可以有共享記憶體的而isolate沒有,這是最為關鍵的差別。
isolate實作簡述
我們可以閱讀Dart源碼裡面的isolate.cc檔案看看isolate的具體實作。
我們可以看到在isolate建立的時候有以下幾個主要步驟:
- 初始化isolate資料結構
- 初始化堆記憶體(Heap)
- 進入新建立的isolate,使用跟isolate一對一的線程運作isolate
- 配置Port
- 配置消息處理機制(Message Handler)
- 配置Debugger,如果有必要的話
- 将isolate注冊到全局監控器(Monitor)
我們看看isolate開始運作的主要代碼
Thread* Isolate::ScheduleThread(bool is_mutator, bool bypass_safepoint) {
// Schedule the thread into the isolate by associating
// a 'Thread' structure with it (this is done while we are holding
// the thread registry lock).
Thread* thread = NULL;
OSThread* os_thread = OSThread::Current();
if (os_thread != NULL) {
MonitorLocker ml(threads_lock(), false);
// Check to make sure we don't already have a mutator thread.
if (is_mutator && scheduled_mutator_thread_ != NULL) {
return NULL;
}
while (!bypass_safepoint && safepoint_handler()->SafepointInProgress()) {
ml.Wait();
}
// Now get a free Thread structure.
thread = thread_registry()->GetFreeThreadLocked(this, is_mutator);
ASSERT(thread != NULL);
// Set up other values and set the TLS value.
thread->isolate_ = this;
ASSERT(heap() != NULL);
thread->heap_ = heap();
thread->set_os_thread(os_thread);
ASSERT(thread->execution_state() == Thread::kThreadInNative);
thread->set_execution_state(Thread::kThreadInVM);
thread->set_safepoint_state(0);
thread->set_vm_tag(VMTag::kVMTagId);
ASSERT(thread->no_safepoint_scope_depth() == 0);
os_thread->set_thread(thread);
if (is_mutator) {
scheduled_mutator_thread_ = thread;
if (this != Dart::vm_isolate()) {
scheduled_mutator_thread_->set_top(heap()->new_space()->top());
scheduled_mutator_thread_->set_end(heap()->new_space()->end());
}
}
Thread::SetCurrent(thread);
os_thread->EnableThreadInterrupts();
thread->ResetHighWatermark();
}
return thread;
}
我們可以看到Dart本身抽象了isolate和thread,實際上底層還是使用作業系統的提供的OSThread。
Flutter Engine Runners與Dart Isolate
有朋友看到這裡可能會問既然Flutter Engine有自己的Runner,那為何還要Dart的Isolate呢,他們之間又是什麼關系呢?
那我們還要從Runner具體的實作說起,Runner是一個抽象概念,我們可以往Runner裡面送出任務,任務被Runner放到它所在的線程去執行,這跟iOS GCD的執行隊列很像。我們檢視iOS Runner的實作實際上裡面是一個loop,這個loop就是
CFRunloop,在iOS平台上Runner具體實作就是CFRunloop。被送出的任務被放到CFRunloop去執行。
Dart的Isolate是Dart虛拟機自己管理的,Flutter Engine無法直接通路。Root Isolate通過Dart的C++調用能力把UI渲染相關的任務送出到UI Runner執行這樣就可以跟Flutter Engine相關子產品進行互動,Flutter UI相關的任務也被送出到UI Runner也可以相應的給Isolate一些事件通知,UI Runner同時也處理來自App方面Native Plugin的任務。
是以簡單來說Dart isolate跟Flutter Runner是互相獨立的,他們通過任務排程機制互相協作。
踩坑血淚史
了解Flutter Engine的原理以及Dart虛拟機的異步實作,讓我們避免采坑,更加靈活高效地進行開發。
在項目應用過程我們踩過不少坑在采坑和填坑的過程中不斷學習。這裡我簡單聊其中一個具體的案例:當時我們需要把Native加載好圖檔資料注冊到Engine裡面去以便生成Texture渲染,使用完資源我們需要将其移除,看起來非常清晰的邏輯竟然造成了野指針問題。後來排查到注冊的時候在一個子線程進行而移除卻在Platform線程進行,在弄清楚線程結構以後問題也就迎刃而解。
結語
本文我們主要讨論了Flutter層面的線程配置管理以及Dart本身isolate的實作。在深入了解Flutter線程機制以後,我們在開發過程當中更加得心應手,同時也啟發我們如何去設計類似應用内的線程結構。
目前我們在探索單個Flutter Engine以元件的方式啟動,也就是多個Flutter Engine執行個體同時存在通過Port來進行通信的可能方案。感興趣或者有相關經驗的朋友歡迎交流,還請不吝賜教。
履歷投遞:[email protected]