對于使用過 Flutter 的開發(fā)來說,應(yīng)該對在 Flutter 混合開發(fā)中,通過 PlatformView
接入原生控件的方式并不陌生,而如果你是從 Flutter 1.20 之前就開始使用 Flutter ,那么應(yīng)該對于 Android 上 PlatformView
的各種體驗問題有過深刻的體會,比如: WebView
里彈出鍵盤的問題。
??注意:文末有驚喜
從一個問題開始
恰巧最近一位朋友在 Flutter 2.10.1 上使用 webview_flutter
和 flutter_pdfview
測試時出現(xiàn)了如下的問題:
attachToContext: GLConsumer is already attached to a context at android.graphics.SurfaceTexture.attachToGLContext(SurfaceTexture.java:289)
所以借著這個問題來給大家科普下 Flutter 里 PlatformView
實現(xiàn)的變遷和未來調(diào)整,首先這個問題的起因是因為:
virtual displayes 和 hybrid composition 兩種
PlatformView
實現(xiàn)混合使用。
因為從 Flutter 2.10 開始,官方的 Plugin 如 webview_flutter
默都是使用 hybrid composition 的實現(xiàn),而第三方的 flutter_pdfview
目前還是使用以前的 virtual display ,這就出現(xiàn)了兩種 PlatformView
實現(xiàn)同時出現(xiàn)的情況。
當(dāng)然,官方在 2.10.2 版本的 #31390 上修復(fù)了這個問題, 問題的原因在于:當(dāng) rasterizer 任務(wù)運行不同的線程時,GrContext
會被重新創(chuàng)建,從而導(dǎo)致 texture
變成沒有初始化的狀態(tài),進(jìn)而重復(fù)調(diào)用 attachToGLContext
導(dǎo)致崩潰。
所以后續(xù)官方修復(fù)這個問題,就是在
attachToGLContext
之前,如果texture
已經(jīng) attach 過,就先調(diào)用detachFromGLContext
進(jìn)行釋放,從而避免了初始化 context 的問題。
但是從問題上看,其實這個問題并不是 2.10 才會出現(xiàn),而是只要在 SurfaceTextureWrapper
這個對象存在時 ,混合使用 virtual displayes 和 hybrid composition 就能引發(fā)這個 bug 。
SurfaceTextureWrapper
是官方用于處理同步的問題,因為當(dāng)SurfaceTexture
被釋放時,由于SurfaceTexture.release
是在 platform 線程被調(diào)用,而attachToGLContext
是在 raster 線程被調(diào)用,不同線程調(diào)用時可能導(dǎo)致:當(dāng)attachToGLContext
被調(diào)用時 texture 已經(jīng)被釋放了,所以需要SurfaceTextureWrapper
用于實現(xiàn) Java 里同步鎖的效果。
所以如果在低版本不想升級,那么可以選擇所有 Plugin 都使用 virtual display 模式或者 hybrid composition 模式,比如 webview_flutter
就提供了 WebView.platform
用于用戶自由選擇 PlatformView
的渲染模式。
當(dāng)然一般情況下我是更建議大家目前都使用 hybrid composition 模式,雖然兩種模式都有潛在問題,但是相比起來目前 virtual display 帶來的性能和鍵盤問題會讓人更難以接受。
區(qū)別和演進(jìn)
其實在之前的 《 Hybrid Composition 深度解析》 里就介紹過它們實現(xiàn)的區(qū)別,這里再結(jié)合上面的問題,從不一樣的角度介紹下它們的實現(xiàn)差異和變遷。
VirtualDisplay
一般 dart 代碼里直接使用 AndroidView
的我們就可以簡單認(rèn)為是使用 virtual display ,比如 flutter_pdfview 1.2.2 版本 , 這種實現(xiàn)方式是 通過將 AndroidView
需要渲染的內(nèi)容繪制到 VirtualDisplays
實現(xiàn)中 ,然后在 VirtualDisplay
對應(yīng)的內(nèi)存里,繪制的畫面就可以通過其 Surface
獲取得到。
VirtualDisplay
類似于一個虛擬顯示區(qū)域,需要結(jié)合DisplayManager
一起調(diào)用,一般在副屏顯示或者錄屏場景下會用到。VirtualDisplay
會將虛擬顯示區(qū)域的內(nèi)容渲染在一個Surface
上。
如上圖所示,簡單來說就是原生控件的內(nèi)容被繪制到內(nèi)存里,然后 Flutter Engine 通過相對應(yīng)的 textureId
就可以獲取到控件的渲染數(shù)據(jù)并顯示出來。
關(guān)于 virtual display 實現(xiàn),如果你需要對應(yīng)路徑去調(diào)試問題,可以參看如下流程:
HybridComposition
使用 hybrid composition 相對會比直接使用 AndroidView
在代碼上更復(fù)雜一點, 需要使用到 PlatformViewLink、 AndroidViewSurface 和 PlatformViewsService 這三個對象,首先我們要創(chuàng)建一個 dart 控件:
- 通過
PlatformViewLink
的viewType
注冊了一個和原生層對應(yīng)的注冊名稱,這和之前的PlatformView
注冊一樣; - 然后在
surfaceFactory
返回一個AndroidViewSurface
用于處理繪制和接收觸摸事件; - 最后在
onCreatePlatformView
方法使用PlatformViewsService
初始化AndroidViewSurface
和初始化所需要的參數(shù),同時通過 Engine 去觸發(fā)原生層的顯示。
Widget build(BuildContext context) {
// This is used in the platform side to register the view.
final String viewType = 'hybrid-view-type';
// Pass parameters to the platform side.
final Map<String, dynamic> creationParams = <String, dynamic>{};
return PlatformViewLink(
viewType: viewType,
surfaceFactory:
(BuildContext context, PlatformViewController controller) {
return AndroidViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
如果通過上面的問題來做個直觀的對比,就會是如下圖所示的變化:
使用 hybrid composition 之后, PlatformView
是通過 FlutterMutatorView
把原生控件 addView
到 FlutterView
上,然后再通過 FlutterImageView
的能力去實現(xiàn)圖層的混合,簡單解釋就是:
Flutter 只直接通過原生的
addView
方法將PlatformView
添加到FlutterView
,這就不需要什么surface
渲染再去獲取的開銷,而當(dāng)你還需要再PlatformView
上渲染 Flutter 自己的 Widget 時,F(xiàn)lutter 就會通過再疊加一個FlutterImageView
來承載這個 Widget 。
舉個例子,如下圖所示,其中:
兩個灰色的 Re 是原生的
TextView
;藍(lán)色、黃色、紅色的是 Flutter 的
Text
;
從渲染結(jié)果上可以看到:
- 灰色的原生
TextView
通過PlatformView
直接就通過原生的addView
方法添加到FlutterView
上; - 而紅色的 Flutter 的
Text
控件因為和PlatformView
沒交集,所以還是 Flutter 原本的渲染邏輯; - 黃色和藍(lán)色的 Flutter 控件,因為和
PlatformView
有交集,所以通過新的FlutterImageView
做承載渲染。
使用 hybrid composition 后,在 Engine 去 SubmitFrame
時,會通過 current_frame_view_count
去對每個 view 畫面進(jìn)行規(guī)劃處理,然后會通過判定區(qū)域內(nèi)是否需要 CreateSurfaceIfNeeded
函數(shù),最終觸發(fā)原生的 createOverlaySurface
方法去創(chuàng)建 FlutterImageView
。
for (const SkRect& overlay_rect : overlay_layers.at(view_id)) {
std::unique_ptr<SurfaceFrame> frame =
CreateSurfaceIfNeeded(context, //
view_id, //
pictures.at(view_id), //
overlay_rect //
);
if (should_submit_current_frame) {
frame->Submit();
}
}
如果有需要調(diào)試 hybrid composition 相關(guān)功能的,可以參考如下路徑, 和 virtual display 不同之處就是在 create
之后的路徑產(chǎn)生了變化 , 更多詳細(xì)演示可見:https://juejin.cn/post/6858473695939084295#heading-2
結(jié)論
所以可以看到,hybrid composition 保留了更多的原生控件效果,也節(jié)省了渲染成本 ,當(dāng)然目前 PlatformView
還有一個比較尖銳的問題,例如 #95343 的閃動問題,這個問題看來在未來會通過更改渲染方式和紋理優(yōu)化來解決。
是的,還是因為性能等問題,所以新的 PlatforView
實現(xiàn)來又要來了,從上面提到的 #31198 已經(jīng)合并可以猜測,下一個穩(wěn)定版本中,現(xiàn)在的 virtual displayes 實現(xiàn)將不復(fù)存在,進(jìn)而替代的是通過新的 TextureLayer
實現(xiàn),未來不排除 hybrid composition 也會被取消,不知道大家此刻心情如何?
簡單說就是:
- 新的
PlatformViewWrapper
會替換掉原本 virtual display 里SurfaceTextureWrapper
相關(guān)的邏輯,通過對輸入的Surface
進(jìn)行lockHardwareCanvas
獲取到Canvas
,再通過super.draw(surfaceCanvas);
進(jìn)行繪制; - 關(guān)于 hybrid composition 目前看起里僅是更換了稱謂,只要核心邏輯沒有大變動;
而如果未來 PlatformViewWrapper
的實現(xiàn)效果良好 ,可以猜測 hybrid composition 模式也會進(jìn)而退出歷史舞臺,所以唯有感慨, Flutter 的技術(shù)演進(jìn)速度真的好快。