掘金x得物公開課 - Flutter 3.0下的混合開發演進
theme: smartblue
hello 大家好,我是《Flutter 開發實戰詳解》的作者,Github GSY 專案的負責人郭樹煜,同時也是今年新晉的 Flutter GDE,藉著本次 Google I/O 之後釋出的 Flutter 3.0,來和大家聊一聊 Flutter 裡混合開發的技術演進。
為什麼混合開發在 Flutter 裡是特殊的存在?因為它渲染的控制元件是通過 Skia 直接和 GPU 互動,也就是說 Flutter 控制元件和平臺無關,甚至連 UI 繪製執行緒都和原生平臺 UI 執行緒是相互獨立,所以甚至於 Flutter 在誕生之初都不支援和原生平臺的控制元件進行混合開發,也就是不支援 WebView
,這就成了當時最大的缺陷之一 。
其實從渲染的角度看 Flutter 更像是一個 2D 遊戲引擎,事實上 Flutter 在這次 Google I/O 也分享了基於 Flutter 的遊戲開發 ToolKit 和第三方工具包 Flame ,如圖所示就是本次 Google I/O 釋出的 Pinball 小遊戲,所以從這些角度上看都可以看出 Flutter 在混合開發的特殊性。
如果說的更形象簡單一點,那就是如何把原生控制元件渲染到
WebView
裡。
最初的社群支援
不支援 WebView
在最初可以說是 Flutter 最大的痛點之一,所以在這樣窘迫的情況下,社群裡湧現出一些臨時的解決方法,比如 flutter_webview_plugin
。
類似 flutter_webview_plugin
的出現,解決了當時大部分時候 App 裡開啟一個網頁的簡單需求,如下圖所示,它的思路就是:
在 Flutter 層面放一個佔位控制元件提供大小,然後原生層在同樣的位置把
WebView
新增進去,從而達到看起來把WebView
整合進去的效果,這個思路在後續也一直被沿用。
這樣的實現方式無疑成本最低速度最快,但是也帶來了很多的侷限性。
相信大家也能想到,因為 Flutter 的所有控制元件都是渲染一個 FlutterView
上,也就是從原生的角度其實是一個單頁面的效果,所以這種脫離 Flutter 渲染樹的新增控制元件的方法,無疑是沒辦法和 Flutter 融合到一起,舉個例子:
- 如圖一所示,從 Flutter 頁面跳到 Native 頁面的時候,開啟動畫無法同步,因為
AppBar
是 Flutter 的,而 Native 是原生層,它們不在同一個渲染樹內,所以無法實現同步的動畫效果 - 如圖二所示,比如在開啟 Native 頁面之後,通過
Appbar
再開啟一個黃色的 Bottm Sheet ,可以看到此時黃色的 Bottm Sheet 打開了,但是卻被 Native 遮擋住(Demo 裡給 Native 設定了透明色),因為 Flutter 的 Bottm Sheet 是被渲染在FlutterView
裡面,而 Native UI 把FlutterView
擋住了,所以新的Flutter UI
自然也被遮擋 - 如圖三所示,當我們通過 reload 重刷 Flutter UI 之後,可以看到 Flutter 得 UI 都被重置了,但是此時 Native UI 還在,因為此時已經沒有返回按鍵之類的無法關閉,這也是這種整合方式一不小心就影響開發的問題
- 如圖四通過 iOS 上的 debug 圖層,我們可以更形象地看到這種方式的實現邏輯和堆疊效果
| 動畫不同步 | 頁面被擋 | reload 之後 | iOS |
| ----------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
| |
|
|
|
PlatformView
隨著 Flutter 的發展,官方支援混合開發勢在必行,所以第一代 PlatformView
的支援還是誕生了,但是由於 Android 和 iOS 平臺特性的不同,最初Android 的 AndroidView
和 iOS 的 UIKitView
實現邏輯相差甚遠,以至於後面 Flutter 的 PlatformView
的每次大調整都是圍繞於 Android 在做優化 。
Android
最初 Flutter 在 Android 上對 PlatformView
的支援是通過 VirtualDisplay
實現,VirtualDisplay
類似於一個虛擬顯示區域,需要結合 DisplayManager
一起呼叫,VirtualDisplay
一般在副屏顯示或者錄屏場景下會用到,而在 Flutter 裡 VirtualDisplay
會將虛擬顯示區域的內容渲染在一個記憶體 Surface
上。
在 Flutter 中通過將 AndroidView
需要渲染的內容繪製到 VirtualDisplays
中 ,然後通過 textureId 在 VirtualDisplay
對應的記憶體中提取繪製的紋理, 簡單看實現邏輯如下圖所示:
這裡其實也是類似於最初社群支援的模式:通過在 Dart 層提供一個
AndroidView
,從而獲取到控制元件所需的大小,位置等引數,當然這裡多了一個textureId
,這個 id 主要是提交給 Flutter Engine ,通過 id Flutter 就可以在渲染時將畫面從記憶體裡提出出來。
iOS
在 iOS 平臺上就不使用類似 VirtualDisplay
的方法,而是通過將 Flutter UI 分為兩個透明紋理來完成組合,這種方式無疑更符合 Flutter 社群的理念,這樣的好處是:
需要在
PlatformView
下方呈現的 Flutter UI 可以被繪製到其下方的紋理;而需要在PlatformView
上方呈現的 Flutter UI 可以被繪製到其上方的紋理, 它們只需要在最後組合起來就可以了。
是不是有點抽象?
簡單看下面這張圖,其實就是通過在 NativeView
的不同層級設定不同的透明圖層,然後把不同位置的控制元件渲染到不同圖層,最終達到組合起來的效果。
那明明這種方法更好,為什麼 Android 不一開始也這樣實現呢?
因為當時在實現思路上, VirtualDisplay
的實現模式並不支援這種模式,因為在 iOS 上框架渲染後系統會有回撥通知,例如:當 iOS 檢視向下移動 2px
時,我們也可以將其列表中的所有其他 Flutter 控制元件也向下渲染 2px
。
但是在 Android 上就沒有任何有關的系統 API,因此無法實現同步輸出的渲染。如果強行以這種方式在 Android 上使用,最終將產生很多如 AndroidView
與 Flutter UI 不同步的問題。
問題
事實上 VirtualDisplay
的實現方式也帶來和很多問題,簡單說兩個大家最直觀的體會:
觸控事件
因為控制元件是被渲染在記憶體裡,雖然你在 UI 上看到它就在那裡,但是事實上它並不在那裡,你點選到的是 FlutterView
,所以使用者產生的觸控事件是直接傳送到 FlutterView
。
所以觸控事件需要在 FlutterView
到 Dart ,再從 Dart 轉發到原生,然後如果原生不處理又要轉發回 Flutter ,如果中間還存在其他派生檢視,事件就很容易出現丟失和無法響應,而這個過程對於 FlutterView
來說,在原生層它只有一個 View 。
所以 Android 的 MotionEvent
在轉化到 Flutter 過程中可能會因為機制的不同,存在某些資訊沒辦法完整轉化的丟失。
文字輸入
一般情況下 AndroidView
是無法獲取到文字輸入,因為 VirtualDisplay
所在的記憶體位置會始終被認為是 unfocused
的狀態。
InputConnections
在unfocused
的 View 中通常是會被丟棄。
所以 Flutter 重寫了 checkInputConnectionProxy
方法,這樣 Android 會認為 FlutterView
是作為 AndroidView
和輸入法編輯器(IME)的代理,這樣 Android 就可以從 FlutterView
中獲取到 InputConnections
然後作用於 AndroidView
上面。
在 Android Q 開始又因為非全域性的
InputMethodManager
需要新的相容
當然還有諸如效能等其他問題,但是至少先有了支援,有了開始才會有後續的進階,在 Flutter 3.0 之前, VirtualDisplay
一直默默在 PlatformView
的背後耕耘。
HybridComposition
時間來到 Flutter 1.2,Hybrid Composition 是在 Flutter 1.2 時釋出的 Android 混合開發實現,它使用了類似 iOS 的實現思路,提供了 Flutter 在 Android 上的另外一種 PlatformView
的實現。
如下圖是在 Dart 層使用 VirtualDisplay
切換到 HybridComposition
模式的區別,最直觀的感受應該是需要寫的 Dart 程式碼變多了。
但是其實 HybridComposition
的實現邏輯是變簡單了: PlatformView
是通過 FlutterMutatorView
把原生控制元件 addView
到 FlutterView
上,然後再通過 FlutterImageView
的能力去實現圖層的混合。
又懵了?不怕,馬上你就懂了
簡單來說就是 HybridComposition
模式會直接把原生控制元件通過 addView
新增到 FlutterView
上 。這時候大家可能會說,咦~這不是和最初的實現一樣嗎?怎麼邏輯又回去了 ?
其實確實是社群的進階版實現,Flutter 直接通過原生的
addView
方法將PlatformView
新增到FlutterView
裡,而當你還需要在PlatformView
上渲染 Flutter 自己的 Widget 時,Flutter 就會通過再疊加一個FlutterImageView
來承載這個 Widget 的紋理。
舉一個簡單的例子,如下圖所示,一個原生的 TextView
被通過 HybridComposition
模式接入到 Flutter 裡(NativeView
),而在 Android 的顯示佈局邊界和 Layout Inspector 上可以清晰看到: 灰色 TextView
通過 FlutterMutatorView
被新增到 FlutterView
上被直接顯示出來 。
所以在 HybridComposition
裡 TextView
是直接在原生程式碼上被 add 到 FlutterView
上,而不是提取紋理。
那如果我們看一個複雜一點的案例,如下圖所示,其中藍色的文字是原生的 TextView
,紅色的文字是 Flutter 的 Text
控制元件,在中間 Layout Inspector 的 3D 圖層下可以清晰看到:
- 兩個藍色的
TextView
是通過FlutterMutatorView
被新增在FlutterView
之上,並且把沒有背景色的紅色 RE 遮擋住了 - 最頂部有背景色的紅色 RE 也是 Flutter 控制元件,但是因為它需要渲染到
TextView
之上,所以這時候多一個FlutterImageView
,它用於承載需要顯示在 Native 控制元件之上的紋理,從而達 Flutter 控制元件“真正”和原生控制元件混合堆疊的效果。
可以看到 Hybrid Composition
上這種實現,能更原汁原味地保流下原生控制元件的事件和特性,因為從原生角度看它就是原生層面的物理堆疊,需要都一個層級就多加一個 FlutterImageView
,同一個層級的 Flutter 控制元件共享一個 FlutterImageView
。
當然,在 HybridComposition
裡 FlutterImageView
也是一個很有故事的物件,由於篇幅原因這裡就不詳細展開,這裡大家可以簡單看這張圖感受下,也就是在有 PlatformView
和沒有 PlatformView
是,Flutter 的渲染會有一個轉化的過程,而在這個變化過程,在 Flutter 3.0 之前可以通過 PlatformViewsService.synchronizeToNativeViewHierarchy(false);
取消。
最後,Hybrid Composition 也不少問題,比如上面的轉化就是為了解決動畫同步問題,當然這個行為也會產生一些效能開銷,例如:
在 Android 10 之前, Hybrid Composition 需要將記憶體中的每個 Flutter 繪製的幀資料複製到主記憶體,之後再從 GPU 渲染複製回來 ,所以也會導致 Hybrid Composition 在 Android 10 之前的效能表現更差,例如在滾動列表裡每個 Item 巢狀一個 Hybrid Composition 的
PlatformView
,就可能會變卡頓甚至閃爍。
其他還有執行緒同步,閃爍等問題,由於篇幅就不詳細展開,如果感興趣的可以詳細看我之前釋出過的 《Flutter 深入探索混合開發的技術演進》 。
TextureLayer
隨著 Flutter 3.0 的釋出,第一代 PlatformView
的實現 VirtualDisplay
被新的 TextureLayer
所替代,如下圖所示,簡單對比 VirtualDisplay
和 TextureLayer
的實現差異,可以看到主要還是在於原生控制元件紋理的提取方式上。
從上圖我們可以得知:
- 從
VirtualDisplay
到TextureLayer
, Plugin 的實現是可以無縫切換,因為主要修改的地方在於底層對於紋理的提取和渲染邏輯; - 以前 Flutter 中會將
AndroidView
需要渲染的內容繪製到VirtualDisplays
,然後在VirtualDisplay
對應的記憶體中,繪製的畫面就可以通過其Surface
獲取得到;現在AndroidView
需要的內容,會通過 View 的draw
方法被繪製到SurfaceTexture
裡,然後同樣通過TextureId
獲取繪製在記憶體的紋理 ;
是不是又有點蒙?簡單說就是不需要繪製到副屏裡,現在直接通過 override View
的 draw
方法就可以了。
在 TextureLayer 的實現裡,同樣是需要把控制元件新增到一個 PlatformViewWrapper
的原生布局控制元件裡,但是這個控制元件通過 override 了 View
的 draw
方法,把原本的 Canvas 替換成 SurfaceTexture
在記憶體的 Canvas ,所以 PlatformViewWrapper
的 child 會把控制元件繪製到記憶體的 SurfaceTexture
上。
舉個例子,還是之前的程式碼,如下圖所示,這時候通過 TextureLayer 模式執行之後,通過 Layout Inspector 的 3D 圖層可以看到,兩個原生的 TextView
通過 PlatformViewWrapper
被新增到 FlutterView
上。
但是不同的是,在 3D 圖層裡看不到 TextView
的內容,因為繪製 TextView
的 Canvas 被替換了,所以 TextView
的內容被繪製到記憶體的 Surface 上,最終會在渲染時同步 Flutter Engine 裡。
看到這裡,你可能也發現了,這時候因為有 PlatformViewWrapper
的存在,點選會被 PlatformViewWrapper
內部攔截,從而也解決了觸控的問題, 而這裡剛好有人提了一個問題,如下圖所示:
"從圖 1 Layout Inspector 看,
PlatformWrapperView
是在FlutterSurfaceView
上方,為什麼如圖 2 所示,點選 Flutter button 卻可以不觸發 native button的點選效果?"。
| 圖1 | 圖2 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| |
|
思考一下,因為最直觀的感受:點選不都是被 PlatformViewWrapper
攔截了嗎?明明 PlatformViewWrapper
是在 FlutterSurfaceView
之上,為什麼 FlutterSurfaceView
裡的 FlutterButton 還能被點選到?
這裡簡單解釋一下:
- 1、首先那個 Button 並不是真的被擺放在那裡,而是通過
PlatformViewWrapper
的super.draw
繪製到 surface 上的,所以在那裡的是PlatformViewWrapper
,而不是 Button ,Button 的內容已經變成紋理去到了FlutterSurfaceView
裡面。 - 2、
PlatformViewWrapper
裡重寫了onInterceptTouchEvent
做了攔截,onInterceptTouchEvent
這個事件是從父控制元件開始往子控制元件傳,因為攔截了所以不會讓 Button 直接響應,然後在PlatformViewWrapper
的onTouchEvent
響應裡是做了點選區域的分發,響應會分發到了AndroidTouchProcessor
之後,會打包發到_unpackPointerDataPacket
進入 Dart - 3、 在 Dart 層的點選區域,如果沒有 Flutter 控制元件響應,會是
_PlatformViewGestureRecognizer
->updateGestureRecognizers
->dispatchPointerEvent
->sendMotionEvent
又傳送回原生層 - 4、回到原生
PlatformViewsController
的createForTextureLayer
裡的onTouch
,執行view.dispatchTouchEvent(event);
總結起來就是:PlatfromViewWrapper
攔截了 Event ,通過 Dart 做二次分發響應,從而實現不同的事件響應 ,它和 VirtualDisplay 的不同是, VirtualDisplay 的事件響應都是在 FlutterView
上,但是TextureLayout 模式,是有獨立的原生 PlatfromViewWrapper
控制元件來開始,所以區域效果和一致性會更好。
問題
最後這裡還需要提個醒,如果你之前使用的外掛使用的是 HybirdComposition
,但是沒做相容,也就是使用的還是 PlatformViewsService.initSurfaceAndroidView
的話,它也會切換成 TextureLayer
的邏輯,所以你需要切換為 PlatformViewsService.initExpensiveAndroidView
,才能繼續使用原本 HybirdComposition
的效果。
⚠️我也比較奇怪為什麼 Flutter 3.0 沒有提及 Android 這個 breaking change ,因為對於開發來說其實是無感的,不小心就掉坑裡。
那你說為什麼還要 HybirdComposition
?
前面我們說過, TextureLayer
是通過在 super.draw
替換 Canvas 的方法去實現繪製,但是它替換不了 Surface
裡的一些 Canvas ,所以比如一些需要 SurfaceView
、TextureView
或者有自己內部特殊 Canvas
的場景,你還是需要 HybirdComposition
,只不過可能會和官方新的 API 名字一樣,它 Expensive 。
Expensive 是因為在 Flutter 3.0 正式版開始,FlutterView
在使用 HybirdComposition
時一定會 converted to FlutterImageView
,這也是 Flutter 3.0 下一個需要注意的點。
更多內容可見 《Flutter 3.0 之 PlatformView :告別 VirtualDisplay ,擁抱 TextureLayer》
最後
最後做個總結,可以看到 Flutter 為了混合開發做了很多的努力,特別是在 Android 上,也是因為歷史埋坑的原因,由於時間關係這裡沒辦法都詳細介紹,但是相信本次之後大家對 Flutter 的 PlatformView
實現都有了全面的瞭解,這對大家在未來使用 Flutter 也會有很好的幫助,如果你還有什麼問題,歡迎交流。
- 給掘金 Logo 快速新增動畫效果,並支援全平臺開發框架
- Flutter 小技巧之優化使用的 BuildContext
- 維護高 Star Github 專案,會遇到什麼有趣的問題 2022 版
- Flutter 小技巧之 ListView 和 PageView 的各種花式巢狀
- Flutter 小技巧之 MediaQuery 和 build 優化你不知道的祕密
- 掘金x得物公開課 - Flutter 3.0下的混合開發演進
- Flutter 小技巧之有趣的動畫技巧
- Flutter 小技巧之 Dart 裡的 List 和 Iterable 你真的搞懂了嗎?
- Flutter 小技巧之玩轉字型渲染和問題修復
- 蘋果 WWDC22 亮點一文彙總解讀,驚喜不停
- Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3
- Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty
- Google I/O Extended | Flutter 遊戲和全平臺正式版支援下 Flutter 的現狀
- 從臺下到臺上,我成為 GDE(谷歌開發者專家) 的經驗分享
- Android 13 適配指南
- Flutter 3.0 之 PlatformView :告別 VirtualDisplay ,擁抱 TextureLayer
- 一文帶你瞭解 Google I/O 2022 精彩彙總與個人感想
- Google I/O 2022:Jetpack 的新功能
- Jetpack Compose 的新功能-谷歌 I/O 2022
- Flutter 3.0 釋出啦~快來看看有什麼新功能-2022 Google I/O