聊一聊Flutter Engine線程管理與Dart Isolate機制

聊一聊Flutter Engine線程管理與Dart Isolate機制

阿里妹導讀:Flutter是一款開源的移動跨平臺UI開發套件,它不僅與現存的Native代碼兼容,還能幫你用Dart語言快速開發高質量的跨平臺App。

本文由閒魚技術團隊福居撰寫,結合Flutter Engine官方文檔討論了Flutter Engine內的線程管理模式以及Dart Isolate機制,希望與大家一起探討。

在終端業務需求日益複雜,版本迭代日趨於頻繁的情況下,我們迫切需要優秀的多端統一跨平臺開發方案以提升研發效率。目前已有類似RN,Weex這種通過JavaScript橋接到Native的終端技術方案。但是,基於JavaScript的橋接模式有JavaScriptCore自身的性能瓶頸和橋接層的消耗。

目前閒魚團隊在積極嘗試和探索Flutter在業務中的實踐以追求更加高效,高性能的跨平臺終端方案。同為跨平臺技術,Flutter有何優勢呢?

  • Flutter在Rlease模式下直接將Dart編譯成本地機器碼,避免了代碼解釋運行的性能消耗。
  • Dart本身針對高頻率循環刷新(如屏幕每秒60幀)在內存層面進行了優化,使得Dart運行時在屏幕繪製實現如魚得水。
  • Flutter實現了自己的圖形繪製避免了Native橋接。

Flutter在應用層使用Dart進行開發,而支撐它的是用C++開發的引擎。

聊一聊Flutter Engine線程管理與Dart Isolate機制

為了更好地應用和實踐,我們需要深入到引擎內部去理解的它的實現原理和構造。線程一直是在開發當中令人比較頭疼的話題,我們也在實踐過程中踩過不少坑,本文就Flutter引擎的線程模式進行一些探討。

Flutter 線程管理

Flutter Engine要求Embeder提供四個Task Runner,Embeder指的是將引擎移植到平臺的中間層代碼。這四個主要的Task Runner包括:

聊一聊Flutter Engine線程管理與Dart Isolate機制

Platform Task Runner

Flutter Engine的主Task Runner,類似於Android Main Thread或者iOS的Main Thread。但是需要注意他們還是有區別的。

一般來說,一個Flutter應用啟動的時候會創建一個Engine實例,Engine創建的時候會創建一個線程供Platform Runner使用。

跟Flutter Engine的所有交互(接口調用)必須在Platform Thread進行,否則可能導致無法預期的異常。這跟iOS UI相關的操作都必須在主線程進行相類似。需要注意的是在Flutter Engine中有很多模塊都是非線程安全的。

規則很簡單,對於Flutter Engine的接口調用都需保證在Platform Thread進行。

阻塞Platform Thread不會直接導致Flutter應用的卡頓(跟iOS android主線程不同)。儘管如此,也不建議在這個Runner執行繁重的操作,長時間卡住Platform Thread應用有可能會被系統Watchdog強殺。

UI Task Runner Thread(Dart Runner)

UI Task Runner用於執行Dart root isolate代碼(isolate我們後面會講到,姑且先簡單理解為Dart VM裡面的線程)。Root isolate比較特殊,它綁定了不少Flutter需要的函數方法,以便進行渲染相關操作。對於每一幀,引擎要做的事情有:

  • 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最終決定繪製到屏幕上的內容。因此這個線程的過載會直接導致卡頓掉幀。

GPU Task Runner

GPU Task Runner主要用於執行設備GPU的指令。UI Task Runner創建的Layer Tree是跨平臺的,它不關心到底由誰來完成繪製。GPU Task Runner負責將Layer Tree提供的信息轉化為平臺可執行的GPU指令。GPU Task Runner同時負責繪製所需要的GPU資源的管理。資源主要包括平臺Framebuffer,Surface,Texture和Buffers等。

一般來說UI Runner和GPU Runner跑在不同的線程。GPU Runner會根據目前幀執行的進度去向UI Runner要求下一幀的數據,在任務繁重的時候可能會告訴UI Runner延遲任務。這種調度機制確保GPU Runner不至於過載,同時也避免了UI Runner不必要的消耗。

建議為每一個Engine實例都新建一個專用的GPU Runner線程。

IO Task Runner

前面討論的幾個Runner對於執行流暢度有比較高的要求。Platform Runner過載可能導致系統WatchDog強殺,UI和GPU Runner過載則可能導致Flutter應用的卡頓。但是GPU線程的一些必要操作,例如IO,放到哪裡執行呢?答案正是IO Runner。

IO Runner的主要功能是從圖片存儲(比如磁盤)中讀取壓縮的圖片格式,將圖片數據進行處理為GPU Runner的渲染做好準備。IO Runner首先要讀取壓縮的圖片二進制數據(比如PNG,JPEG),將其解壓轉換成GPU能夠處理的格式然後將數據上傳到GPU。

獲取諸如ui.Image這樣的資源只有通過async call去調用,當調用發生的時候Flutter Framework告訴IO Runner進行加載的異步操作。

IO Runner直接決定了圖片和其它一些資源加載的延遲間接影響性能。所以建議為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是共享的。引擎源碼如下:

聊一聊Flutter Engine線程管理與Dart Isolate機制

這裡我們可以進行改動,讓引擎每個實例初始化獨自的線程:

聊一聊Flutter Engine線程管理與Dart Isolate機制

理論上你可以配置任意線程供其使用,不過最好遵循最佳實踐。

具體代碼導讀

iOS Android平臺可以參考Flutter Engine源碼:

聊一聊Flutter Engine線程管理與Dart Isolate機制

Dart isolate機制

聊一聊Flutter Engine線程管理與Dart 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開始運行的主要代碼:

聊一聊Flutter Engine線程管理與Dart Isolate機制

我們可以看到Dart本身抽象了isolate和thread,實際上底層還是使用操作系統的提供的OSThread。

聊一聊Flutter Engine線程管理與Dart Isolate機制

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 Engine以組件的方式啟動,多個Flutter Engine實例同時存在通過Port來進行通信的可能方案。歡迎感興趣的朋友一起交流!


https://github.com/flutter/flutter

https://github.com/flutter/engine

https://flutter.io/

https://www.dartlang.org/

《Dart編程語言》

聊一聊Flutter Engine線程管理與Dart Isolate機制

每天一篇技術文章,

看不過癮?

發現更多AI乾貨。


分享到:


相關文章: