# 前言
官方宣称,Flutter 3.0 是一个重大的版本更新,它带来了许多新功能和改进,其中最值得关注的是对 Flutter 的桌面稳定支持。但,这对 PlatformView 功能来说似乎是个例外:
-
截止Flutter 3.7.9,PlatformView仍然仅支持移动端,桌面端尚不可用;
-
Android端新引入的TLHC模式,使得PlatformView的模式选择变得异常混乱和复杂;
-
iOS的可变刷新率暴露了Hybrid Composition模式的缺陷;
本文首先会沿着PlatformView方案的演进历史,剖析各个方案的实现原理和优缺点,给出最佳实践,帮助读者理解和正确选择PlatformView的渲染模式。接着会介绍Hummer在FY23中所做的优化和探索。最后,会聊聊对PlatformView未来工作的规划。
# PlatformView演进历史
Flutter框架将开发人员描述的Widget树转换为内部层次结构,并决定实际渲染的像素。它控制纹理并直接渲染,而不使用原生视图层次结构。这意味着默认情况下,Flutter UI不会包含原生视图。这对于希望在Flutter应用程序中包含复杂原生视图的开发人员来说是一个问题。
为了解决这个问题,Flutter引入了PlatformView功能,以便开发者复用现有的原生视图,例如,地图或WebView。
但是,当前的Flutter架构是将整个Widget树都渲染到了单个纹理上,如何才能将原生视图嵌入到Flutter Widget树的内部层次结构中,并在它们之间交错呢?
下面,让我们一起探究 PlatformView 的演进历史,并深入分析各个方案的实现原理及优缺点。
## Android端
按照时间先后顺序,Android端实现了三种PlatformView模式:
-
Virtual Display(VD)
-
Hybrid Composition(HC)
-
Texture Layer Hybrid Composition(TLHC)
每种后续模式都旨在解决前一种模式的缺陷。下面将详细介绍每种模式的实现原理和存在的问题。
### Virtual Display(VD)
Virtual Display是Android端PlatformView的最早实现方案,解决了开发者在Flutter应用中嵌入原生视图的强烈诉求。
#### 实现原理
| |
图(1) | 图(2) |
如图(1)所示,Virtual Display模式是通过将原生视图渲染到Virtual Display中来实现的。Virtual Display类似于一个虚拟显示区域,它将虚拟显示区域的内容(原生视图)渲染到一个Surface上。然后,Flutter引擎可以通过相应的texture Id获取到原生视图的渲染数据,并将其与自己内部的Widget树的其余部分进行合成,最后作为Flutter在Android上更大纹理输出的一部分进行渲染。
#### 方案缺陷
该方案确实解决了将Android视图嵌入Flutter的诉求,但也存在一些严重的缺陷:
-
如图(2)所示,Android视图位于Virtual Display自己独立的View层次结构中,并且与包含Flutter UI的View层次结构完全隔离,View相关信息不能被正确获取。因此这种方法会带来难以解决的功能问题,例如,触摸事件、文本输入、辅助功能等;
-
Virtual Display 上的 Window 绘制是在 Primary Display之后,输出会有1帧的延迟;
-
原生视图的每个像素都流经额外的中间图形缓冲区,对内存和性能有一定影响;
#### 使用方式
在TLHC模式上线前(Flutter3.0之前),开发者可以简单地通过返回AndroidView(Flutter框架会隐式地调用initAndroidView方法创建一个TextureAndroidViewController对象),显式地选择Virtual Display模式。
示例代码如下:
Widget build(BuildContext context) {
// This is used in the platform side to register the view.
const String viewType = '<platform-view-type>';
// Pass parameters to the platform side.
final Map<String, dynamic> creationParams = <String, dynamic>{};
return AndroidView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
### Hybrid Composition(HC)
为了解决了Virtual Display的问题,特别是触摸事件、文本输入、辅助功能等功能性问题。在Flutter 1.20.0中,官方引入了Hybrid Composition模式。
#### 实现原理
与「Virtual Display」不同,该模式直接在视图层次结构中显示原生Android视图:
-
如图(3)所示,Flutter widget树被分为两个不同的Android原生视图(FlutterImageView),一个在platform view下方,一个在其上方。具体方案是,通过ImageReader获取Flutter UI的输出,并将其转为FlutterImageView,然后将其与原生Android视图一起添加到视图层次结构中,进行渲染。
-
为了避免撕裂或其他视觉残影,Flutter的合成必须在platform线程上完成,而不是raster线程,所以需要将raster线程合并到platform线程。
由于原生视图直接显示,就像在非Flutter应用程序中一样,因此,该模式具有非常好的兼容性。
| |
图(3) | 图(4) |
#### 方案缺陷
该方案确实解决了「Virtual Display」的痛点,但是自身也存在一定缺陷:
-
在SDK 29(Android 10)之前的Android版本上,由于不支持通过HardwareBuffer直接创建Bitmap,Flutter帧需要进行GPU->CPU->GPU往返拷贝,这会严重影响性能。(https://github.com/googleads/googleads-mobile-flutter/issues/269)
-
Raster线程与Platform线程合并带来了潜在的性能损失,以及死锁问题(https://github.com/flutter/flutter/issues/94524)
#### 使用方式
在TLHC模式上线前(Flutter3.0之前),开发者可以简单地通过initSurfaceAndroidView接口创建一个SurfaceAndroidViewController对象来显式地选择Hybrid Composition模式。
示例代码如下:
Widget build(BuildContext context) {
// This is used in the platform side to register the view.
const String viewType = '<platform-view-type>';
// Pass parameters to the platform side.
const Map<String, dynamic> creationParams = <String, dynamic>{};
return PlatformViewLink(
viewType: viewType,
surfaceFactory:
(context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
### Texture Layer Hybrid Composition(TLHC)
Hybrid Composition(HC)解决了Virtual Display(VD)的问题,但因自身的性能问题饱受诟病。于是,Flutter 3.0又推出了Texture Layer Hybrid Composition(TLHC)方案。
TLHC最初的目的是完全替代HC和VD。一度,VD的代码被官方移除,HC也默认不能使用(这是导致Android端模式选择混乱复杂的原因)。然而,事实证明TLHC存在一些限制,无法完全替代它们。
#### 实现原理
| |
图(5) | 图(6) |
如图(5)所示,TLHC使用自定义的FrameLayout,它与Virtual Display类似,将Android View绘制重定向到支持Flutter纹理的canvas上;然后,Flutter引擎将该纹理与Flutter UI的其余部分进行合成(无需Hybrid Composition的分层和线程复杂性);最后,将其一起输出。
#### 方案缺陷
该方案性能表现良好,大多数情况下可以作为开发者首选,但是存在一定「兼容性」问题:
-
要求API level>= 23 (Android 6.0或以上)
由于涉及Surface#lockHardwareCanvas API,这需要SDK级别23或更高版本。 -
不支持Android SurfaceView
Android SurfaceView绕过了Android视图的正常绘制机制,因此没有按预期重定向到指定纹理。如果PlatformView本身是或包含SurfaceView,该模式将无法正常工作,SurfaceView将被绘制在错误的位置和/或z-index。为了缓解该问题,当SurfaceView存在时,PlatformView会尝试自动回退到VD或HC。但,这仅在创建PlatformView时SurfaceView存在时才有效。 -
Hummer发现并修复的兼容性问题:
-
在部分Android Q上,PlatformView显示白屏([pull/31698](https://github.com/flutter/engine/pull/31698))
-
onTrimMemory时,Android Q及以上机型出现崩溃([pull/33655](https://github.com/flutter/engine/pull/33655))
-
其他问题,例如,原生视图与Flutter UI显示不同步的问题([issues/121686](https://github.com/flutter/flutter/issues/121686))
-
#### 使用方式
非常遗憾的是,TLHC方案上线后,并没有为新的模式增加新接口,而是粗暴地将现有的HC和VD的接口的底层实现全部修改为走TLHC方案。当发现TLHC不能完全替换VD和HC时,为了处理兼容性问题,官方又增加了一系列的回退方案,这直接导致了,如今的模式选择非常混乱和复杂。
总结来说:
-
先前的 VD 和 HC 接口现在默认使用 TLHC 模式,但在特定情况下可能会自动回退到 VD 和 HC 模式(具体细节,请查看示例代码中的注释);
-
新增了 initExpensiveAndroidView 接口,用于显式指定使用 HC 模式;
-
VD 模式仅用于回退,开发者无法显式指定使用;
示例代码如下:
if (usingAndroidViewSurface) {
return PlatformViewLink(
viewType: viewType,
surfaceFactory: (context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <
Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (params) {
if (usingHybridComposition) {
// 1. 开发者显式指定始终使用「Hybrid composition」模式
// (该API是在Flutter 3.0中引入的,始终使用HC模式)
return PlatformViewsService.initExpensiveAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(
params.onPlatformViewCreated)
..create();
} else {
// 2. 引擎默认使用Texture Layer Hybrid composition模式;
// 如果不支持,则自动fallback回「Hybrid composition」模式。
// 如果当前SDK版本<23或平台视图层次结构在创建时包含SurfaceView(或子类),则触发回退。
//
// 【已知问题】:
// 如果视图层次结构在创建时不包含SurfaceView,但稍后添加了一个,则渲染将无法正常工作;
//
// 【解决方案】
// 1. 在创建时在视图层次结构中包含一个0x0的SurfaceView,以触发回退到HC;
// 2. 或者切换到initExpensiveAndroidView显式指定HC;
// (上述行为适用于Flutter 3.7+。Flutter 3.0未包括回退到HC,在Flutter <3.0中始终使用HC。)
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(
params.onPlatformViewCreated)
..create();
}
},
);
} else {
// 3. 默认使用Texture Layer Hybrid composition模式;
// 如果不支持,则自动回退到「Virtual display」模式。
// 如果当前SDK版本<23或平台视图层次结构在创建时包含SurfaceView(或子类),则触发回退。
//
// 【已知问题】:
// 如果视图层次结构在创建时不包含SurfaceView,但稍后添加了一个,则渲染将无法正常工作;
//
// 【解决方案】
// 1. 在创建时在视图层次结构中包含一个0x0的SurfaceView,以触发回退到VD;
// 2. 或者切换到initExpensiveAndroidView显式指定HC;
// (上述行为适用于Flutter 3.3+。Flutter 3.0未包括回退到VD,在Flutter <3.0中始终使用VD。)
return AndroidView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
## IOS
### Hybrid Composition(HC)
在iOS端,PlatformView只有Hybrid Composition一种实现方案,从Flutter 1.22开始默认启用,不再需要在Info.plist中添加io.flutter.embedded_views_preview标志。
#### 实现原理
|
图(7) |
与Android端的HC方案类似,嵌入的原生视图将Flutter UI分成多个部分,最下面的部分被绘制到FlutterView,上面的多个部分被渲染到多个FlutterOverlayView上(取决于Flutter UI和原生视图之间的交错层数)。
当然,同样也需要动态合并线程,以解决渲染同步问题。
#### 方案缺陷
与Android端类似,合并线程带来了一些问题:
1. 性能问题,例如,高刷机抖动问题([#issues/116640](https://github.com/flutter/flutter/issues/116640))
2. 死锁风险,例如,[#issues/94524](https://github.com/flutter/flutter/issues/94524)
#### 使用方式
Widget build(BuildContext context) {
// This is used in the platform side to register the view.
const String viewType = '<platform-view-type>';
// Pass parameters to the platform side.
final Map<String, dynamic> creationParams = <String, dynamic>{};
return UiKitView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
## 挖洞模式
为了解决当时Virtual Display的性能问题,Hummer在1.17.4版本上线了「挖洞模式」渲染方案,支持Android和iOS双端,取得了不错的效果。
### 实现原理
挖洞模式不需要合并线程,简单高效:
1. 将原生视图放到FlutterView的下方,并将相应位置透明化处理(挖洞),这样原生视图与Flutter UI的渲染互不干扰,但从用户视觉上看是一个整体;
2. 在Raster线程中计算原生视图的位置、大小、透明度等信息,并在合适时机通知Platform线程更新,以此做到线程分离,并优化了原生视图与Flutter UI的显示同步;
| |
图(8) | 图(9) |
### 方案缺陷
与官方的方案相比,挖洞模式 在性能和兼容性等方面都有明显的优势。
但,暂不支持半透明混合,可能导致部分场景使用受限。
### 使用方式
在Hummer3.0上,「挖洞模式」不再默认开启,需要开发者显式指定。为了兼容Hummer3.0以下版本和官方的多种渲染模式,我们做了以下修改:
1)保留了前一个版本的`renderType`参数;
2)新增`initSimpleAndroidView`接口;
具体使用方式,请查看下面示例代码和注释:
@override
Widget build(BuildContext context) {
// Pass parameters to the platform side.
final Map<String, dynamic> creationParams = <String, dynamic>{
'PenetratedDisplay': usingPenetratedDisplay,
};
switch (defaultTargetPlatform) {
case TargetPlatform.android:
if (usingAndroidViewSurface) {
return PlatformViewLink(
viewType: viewType,
surfaceFactory: (context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <
Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (params) {
if (usingPenetratedDisplay) {
// 1. 显式使用「挖洞模式」(Hummer3.0新增)
return PlatformViewsService.initSimpleAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(
params.onPlatformViewCreated)
..create();
} else if (usingHybridComposition) {
// 2. 显式使用「Hybrid composition」模式
return PlatformViewsService.initExpensiveAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(
params.onPlatformViewCreated)
..create();
} else {
// 3. 默认使用Texture Layer Hybrid composition模式;
// 如果不支持(例如,SDK_INT小于23或者包含surface view),
// 则自动fallback回「Hybrid composition」模式。
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(
params.onPlatformViewCreated)
..create();
}
},
);
} else {
// 4. 默认使用Texture Layer Hybrid composition模式;
// 如果不支持(例如,SDK_INT小于23或者包含surface view),
// 则自动fallback回「Virtual display」。
return AndroidView(
viewType: viewType,
// 5. 新增「挖洞模式」可选参数
renderType: (usingPenetratedDisplay
? PlatformViewRenderType.penetratedDisplay
: null),
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
case TargetPlatform.iOS:
return UiKitView(
viewType: viewType,
// 6. 新增「挖洞模式」可选参数
renderType: (usingPenetratedDisplay
? PlatformViewRenderType.penetratedDisplay
: null),
layoutDirection: TextDirection.ltr,
creationParams: const <String, dynamic>{},
creationParamsCodec: const StandardMessageCodec(),
);
default:
throw UnsupportedError('Unsupported platform view');
}
}
# PlatformView模式选择
## 模式汇总
在前面的章节中,我们梳理了 PlatformView 方案的演进历史,深入分析了各个方案的实现原理和优缺点,现将其整理如下,以供参考:
嵌入方案 | 优 点 | 缺 点 | 支持平台 |
VD | —— | 1. 辅助功能、触摸事件、文本输入等问题比较多; 2. Flutter 3.0后,该模式只会被动fallback,不能主动选择 | Android |
HC | 兼容性好 | 1. 线程合并引发的系列问题;2. AndroidQ以下机型性能差 | Android & iOS |
TLHC | 性能次优 | 兼容比较性差,根据下面两种情况会「自动」fallback到VC或HC模式: 1. 不支持SurfaceView 2. 不支持Android6.0以下的机型 | Android |
挖洞模式 | 性能最佳 | 不支持半透明混合 | Android & iOS |
【注】也可以基于 DrawFunctor 来实现PlatformView,其优点是内存占用少,可以实现PlatformView与Flutter UI的严格帧同步,但,它的最大的弊端是存在「系统兼容性问题」。
## 选择建议
对于开发者如何选择这些模式,这里给出以下一般性建议,遇特殊情况还需酌情考虑:
-
交互复杂,对兼容性要求高的场景,优先考虑 Hybrid Composition;
-
不需要半透明混合,对性能要求比较高的场景,优先尝试 挖洞模式;
-
在Android端,其他场景使用TLHC,需注意fallback到VD或HC的影响;
# PlatformView优化和新方案探索
在刚过去FY23财年,Hummer在PlatformView方面做了如下几个方面的工作:
-
积极参与Flutter社区的沟通和交流, 结合我们的业务,打磨和优化现有功能;
-
挖洞模式代码重构和功能增强;
-
新渲染方案的研究和探索;
-
桌面端的调研和验证;
## 社区贡献
Flutter官方PlatformView方案的在不断演进和迭代,Hummer一如既往地深度参与其中,与社区一起推动PlatformView走向更好。下面列举了部分Hummer发现和解决的问题:
### 1. 部分Android Q上,TLHC模式显示白屏([#pull/31698](https://github.com/flutter/engine/pull/31698))
该问题非常隐晦,很难跟踪。最终的原因是,在部分Android Q手机上,主要是Android Q早期版本,因为兼容性问题,系统SurfaceTexture要求强制读写同步,也就是没有额外的一帧缓冲,前一帧消费掉后才允许提交新的一帧。否则,接下来向BufferQueue请求帧缓冲都会失败,从而导致PlatformView区域显示白屏。
### 2. onTrimMemory触发时,Android Q及以上机型出现崩溃([#pull/33655](https://github.com/flutter/engine/pull/33655))
这是TLHC的另一严重的高崩问题。该问题的原因是,系统在收到低内存通知(level=80)时,Android Q及以上的系统会释放底层的Surface,而不会通知上层(也没有可用的通知渠道)。
### 3. 其他问题
除了上述问题,在Hummer 3.0升级过程中我们还发现了其他一些问题,其中大部分还没来得及去分析:
1. [[Android & HC] 在共享引擎的场景下,打开新页面或后退闪现白屏](https://github.com/flutter/flutter/issues/97188)
2. [[Android & HC] 在共享引擎的场景下,打开新页面会残留前一个页面的PlatformView](https://github.com/flutter/flutter/issues/113826)
3. [[iOS] 滑动包含PlatformView的页面,页面顶部闪现白色区域](https://github.com/flutter/flutter/issues/119485)
4. [[Android & TLHC] 打开新页面时,PlatformView比Flutter UI晚几帧显示](https://github.com/flutter/flutter/issues/121686)
5. [[Android & HC] 后退时,PlatformView比Flutter UI晚几帧消失](https://github.com/flutter/flutter/issues/121687)
6. ~~[[iOS] 在高刷机上滑动PlatformView列表页面出现严重抖动](https://github.com/flutter/flutter/issues/116640)~~
## 挖洞模式优化
Hummer的挖洞模式简单高效,仍有很大的价值,有进一步优化使用体验的必要。在Hummer3.0中,我们对挖洞模式进行了重大重构,具体如下:
-
优化了设计,方便后续维护和升级;
-
支持缩放/旋转/圆角等矩阵变换;
-
优化了滑动体验;
-
优化了开发者接口;
### 支持缩放/旋转/圆角
Android | iOS |
,时长00:30 | ,时长00:22 |
视频(1) | 视频(2) |
注:在视频中,PlatformView背景颜色变化是手势点击触发的。
### 解决iOS滑动残影
优化前 | 优化后 |
,时长00:06 | ,时长00:06 |
视频(3) | 视频(4) |
## 桌面端调研
Flutter的 2023年路线图([Roadmap](https://github.com/flutter/flutter/wiki/Roadmap))规划了今年可能会实现的一些重要功能,其中就明确包括了PlatformView。
但是,截止目前(Flutter 3.7.9)桌面端的PlatformView仍不可用,仅对macOS有了非常有限的支持,Windows还无任何代码提交,Linux平台暂无明确计划。看起来,Flutter的大部分人力都投入到了impeller和web中去了,可能人力不太足~
macOS目前仅支持嵌入简单原生视图,不支持半透明、旋转、缩放、圆角等变换,也不支持与Flutter UI交错渲染。下图是我们在macOS上的测试效果([demo源码](https://github.com/0xZOne/platform_view_macos_example)):
| |
图(14) | 图(15) |
# 结语
Flutter自2018年开始在移动端支持PlatformView,并经过几年的迭代演进,日趋完善,形成了现在多种方案共存的局面。由于没有一种方案能覆盖所有场景,需要业务开发者熟悉每种方案的优缺点,并根据不同场景选择合适的方案。Hummer接下来在移动端所需要做的事情是,结合业务对现有方案进行完善和优化,特别是官方没太重视的混合栈场景。
对于桌面端,Hummer才刚扬帆起航,还没有收到业务对PlatformView功能的强烈诉求,我们会提前做些技术储备。具体而言,一方面,我们会密切关注官方桌面端PlatformView的进展,做好随时切入的准备;另一方面,会考虑将简单高效的挖洞模式移植到桌面端。
PlatformView功能涉及多端,加上混合栈场景,很多问题复现和处理起来并不容易。欢迎大家一起共建和探讨~