深入理解Flutter引擎線程模式
作者:閑魚技術-福居
在終端業務需求日益複雜,版本迭代日趨於頻繁的情況下,我們迫切需要優秀的多端統一跨平台開發方案以提升研發效率。目前已有類似RN,Weex這種通過JavaScript橋接到Native的終端技術方案。但是,基於JavaScript的橋接模式有JavaScriptCore自身的性能瓶頸和橋接層的消耗。閑魚產品界面的複雜度和我們制定的高性能基線使得我們無法選擇這些方案。
目前我們正在積極嘗試和探索Flutter在業務中的實踐以追求更加高性能的跨平台終端方案。同為跨平台技術,Flutter為什麼有其它方案所不具備的高性能特性?
- Flutter在Rlease模式下直接將Dart編譯成本地機器碼,避免了代碼解釋運行的性能消耗。
- Dart本身針對高頻率循環刷新(如屏幕每秒60幀)在內存層面進行了優化,使得Dart運行時在屏幕繪製實現如魚得水。
- Flutter實現了自己的圖形繪製避免了Native橋接。
Flutter體系結構:
為了更好地應用和實踐,我們需要深入到引擎內部去理解的它的實現原理和構造。線程一直是在開發當中令人比較頭疼的話題,我們也在實踐過程中踩過不少坑,本文就Flutter引擎的線程模式進行一些探討。
對於剛接觸Flutter的朋友,更加詳盡的Flutter相關信息可以瀏覽它的官方網站:Flutter IO
Flutter 線程模型
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.ccflutter/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源碼裡面的http://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 dont 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設計的過程中,我們得到啟發如何去設計類似應用內的線程結構。
延展討論
目前團隊將Flutter集成到現有的App是以單例的方式啟動的,這樣避免了Native與Flutter頁面切換過程當中由於Flutter冷啟動導致的耗時問題。但是單引擎模式啟動在支持多個Flutter頁面的時候邏輯變得複雜。最為理想的狀態,我們希望每一個頁面都是一個相對獨立的組件,這可能需要引擎層面進行一些改造。一種可能設想是一個組件對應一個引擎實例,多實例的引擎在線程模式方面面臨一些挑戰。包括線程復用,線程間的通信,線程性能調優,引擎間的協作的問題。
我們將在這方面繼續深入的探索和嘗試,請後續關注我們公眾號發布的文章。另外,閑魚期待你的加入!
http://weixin.qq.com/r/Pi4nIyXEpO3YKWFAb3u6 (二維碼自動識別)
參考資料
- Flutter 開發文檔
- Flutter 引擎文檔
- Flutter IO
- Dart 語言開發文檔
- 《Dart編程語言》
推薦閱讀: