hello 我们好,我是《Flutter 合作开发两栖作战简述》的译者,Github GSY 工程项目的相关人士郭树煜,与此同时也是去年名星的 Flutter GDE,话虽如此此次 Google I/O 后正式发布的 Flutter 3.0,来和我们聊一聊 Flutter 里混和合作开发的控制技术重构。
为何混和合作开发在 Flutter 里是特定的存有?即使它图形的命令行是透过 Skia 间接和 GPU 可视化,也是说 Flutter 命令行和网络平台毫无关系,即使连 UI 绘出缓存都和原生植物网络平台 UI 缓存是互相分立,因此班莱班县 Flutter 在问世Hathras都不全力支持和原生植物网络平台的命令行展开混和合作开发,也是不全力支持 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 层提供两个 AndroidViewtextureId,这个 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 过程中可能会即使机制的不同,存有某些信息没办法完整转化的丢失。
文字输入
一般情况下 AndroidViewVirtualDisplay 所在的内存位置会始终被认为是 unfocused 的状态。
InputConnections 在 unfocused 的 View 中通常是会被丢弃。
因此 Flutter 重写了 checkInputConnectionProxy方式,这种 Android 会认为FlutterView 是作为 AndroidView 和输入法编辑器(IME)的代理,这种 Android 就能从 FlutterViewInputConnections 然后作用于 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 进入 Dart3、 在 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 也会有很好的帮助,如果你还有什么问题,欢迎交流。