在 Flutter 里嵌 WebView、地图、相机,等于让 两套渲染器争同一块屏幕。这不是「再包一个 Widget」那么便宜。Flutter 的默认假设是:Raster 线程独占 GPU,一帧的像素由 Impeller/Skia 画完,交给系统合成器。Platform View 打破的就是这条假设。三代方案的差别不在 Widget 名字,而在 原生 View 的像素从哪条路径进入 Flutter 的 Layer 树,以及输入法、手势、合成器同步点落在哪里。

读完你应该能独立回答三个问题:Virtual Display 为什么伤显存和焦点、Hybrid Composition 贵在几次合成、Texture Layer 仍然掉帧时该查哪条同步。

1. 先画边界:这是 Engine 最贵的例外

Framework 侧你看见的是 AndroidView / UiKitView / AndroidViewSurface / PlatformViewLink。它们都只是把一个 int viewId 和工厂回调交给 Engine。真正花钱的是 Embedder:

  • 原生 View 的窗口 / Surface / 纹理从哪来
  • 它的绘制和 Flutter 的 swapchain 如何同步(fence)
  • 触摸先给 Flutter 还是先给原生
  • IME、无障碍、截屏、截图谁能看见这块像素

不变量:

  • Flutter 不再「一条 Raster 线程画完所有像素」。多了至少一次跨渲染器的汇合。
  • 汇合要么发生在 系统合成器(多 Surface 叠层),要么发生在 Flutter 合成器(把原生帧当纹理采样)。
  • 手势竞技场到这块矩形的边缘失效。原生 View 内部是另一套命中测试。
  • 生命周期跟的是原生 View,不是 Element。Element unmount 必须把原生 View detach,否则窗口泄漏。

能用纯 Flutter 画出来的控件,不要嵌原生。这不是洁癖,是避免例外路径。

2. 三代方案:像素怎么汇合

Virtual Display(VD)

Android 早期方案。Engine 申请一块 VirtualDisplay,让原生 View 画到这块虚拟屏的 Surface 上,再把结果当纹理贴进 Flutter Layer 树。

代价是刚性的:

  • 虚拟屏有自己的分辨率和 DPI,和设备屏再采样一次,文字容易糊。
  • 多一块离屏 GPU 缓冲,显存按 宽 × 高 × 4 × 缓冲数 涨。
  • 输入法和焦点要跨虚拟屏,历史 bug 多:键盘弹不出来、IME 高度不对、光标错位。
  • 无障碍桥要再映射一层。

它实现简单,所以曾经是默认。现在应视为淘汰路径,只在旧设备 fallback 时出现。

Hybrid Composition(HC)

把 Flutter 自己的画面拆成多个原生 Surface(或 FlutterImageView),按 z 序插入:底下一块 Flutter、中间原生 View、上面再一块 Flutter(给悬浮按钮、工具栏)。系统合成器做最终叠层。

换来的是:原生 View 仍是「真 View」,IME、焦点、无障碍走系统默认路径,WebView 输入终于正常。

代价转到合成:

  • 每多一块 Surface,系统合成器多一次 composition。部分 Android 设备(尤其是 OEM 的 Overlay 数量限制)会从 Overlay 掉到 GPU composition,整页掉帧。
  • 圆角 clip、半透明、transform 动画迫使系统没法用 Overlay,退回 GPU。
  • Flutter 侧「上面那一块」要和原生 View 对齐像素,滚动时容易出现一帧缝。
  • iOS 的 UiKitView 本质上长期接近这种叠层:UIView 插在 FlutterView 层级里。

Texture Layer Hybrid Composition(TLHC)

原生 View 画进一块 共享 GPU 纹理(或 SurfaceTexture / HardwareBuffer),Flutter 把它当 TextureLayer 采样,重新回到「单 swapchain 合成」。系统合成器只看见 Flutter 的那张最终 buffer。

这通常是 Android 10+ 的首选:少一次整窗拆 Surface,帧率和内存更好,圆角和透明度可以当普通 Layer 来 clip。

仍然要付的账:

  • 纹理更新频率。相机 30fps 更新纹理,Flutter 60fps 采样,要靠 fence 等 GPU 写完。等久了掉帧,不等会撕。
  • 尺寸变化、旋转、DPI 改变要重建纹理。动画里改 Platform View 的 size 等于每帧 alloc 纹理。
  • 某些效果(需要「真 View」才能做的截屏、部分 WebView 硬件加速路径)会强制 fallback 回 HC。
  • 输入法:纹理不是 View,焦点和 IME 仍要额外桥。TLHC 在这方面比 VD 好、比 HC 脆,具体随版本变。
方案像素汇合点IME / 焦点显存合成开销现状
VD虚拟屏 → 纹理差高中淘汰 / fallback
HC系统合成器多 Surface好中高(设备相关)iOS 默认思路;Android 特定场景
TLHCFlutter 纹理采样中好较低较低Android 优先

3. 线程与同步点

Platform View 把线程模型从「UI 记账、Raster 上屏」扩成至少四家:

  • UI Runner: 布局出一块矩形,Layer 树上挂 PlatformViewLayer 或 TextureLayer。
  • Platform Runner: 创建/布局原生 View,转发触摸,处理 IME。这块在系统主线程,卡了就是 ANR。
  • 原生 View 自己的渲染线程: WebView、地图、相机各有各的。你控制不了。
  • Raster Runner: 采样纹理或提交拆开的 Surface。

同步靠 GPU fence 或 Surface 的 dequeue/queue。不变量:不要在 UI Runner 上等原生 View 画完。 布局只决定矩形;像素可以晚一拍。追求「原生和 Flutter 像素同一帧对齐」会把流水线锁死,表现为两者一起掉帧。

触摸路径更乱。一次 down:

  1. 系统先给 Flutter 的根 View。
  2. Flutter HitTest 若打中 Platform View 矩形,Embedder 决定是否 claim 并把后续事件转给原生。
  3. 原生内部再 HitTest。
  4. 若 Flutter 同时有自己的手势(例如外层 Scrollable),两边都想跟手。Android 上靠 PlatformViewGestureMixin / eager 策略;iOS 上靠 gestureRecognizersBlockingPolicy。

竞技场规则到这里失效。你不能指望 VerticalDragGestureRecognizer 和 WebView 内部的滚动自动互斥。必须显式:要么让 Platform View 在 down 时 eager claim(内部可滚),要么让 Flutter 父级滚、原生不抢。两边都可滚是最糟的产品状态。

4. 内存与生命周期

原生 View 活在 Java/ObjC 堆,纹理活在 GPU,Layer 活在 C++ 堆,Element 活在 Dart 堆。四块要一起拆:

  • Element unmount → Controller dispose → Embedder 从层级移除 View → 释放 Surface / 纹理。
  • 只 setState 把 AndroidView 从树里拿掉却缓存了 controller,View 会成为「看不见的窗口」,继续耗电、接触摸。
  • 列表里 AndroidView:滚动出视口应销毁或至少 keepAlive 有上限。同时存在的 WebView 数量是内存峰值,不是「缓存策略能抹平」的那种。
  • AutomaticKeepAliveClientMixin 对 Platform View 很危险:它会把原生实例钉住。

显存估算不要看 Widget 数量。一块 1080×1920 的 WebView,RGBA 单缓冲约 8MB,双/三缓冲再乘。HC 模式下 Flutter 上下两层各一份;VD 再加虚拟屏。列表里三个全屏级 Platform View,百 MB 显存是正常数量级。

iOS 的 UiKitView 还参与 Auto Layout。在 performLayout 对应的那次主线程布局里改 UIView frame,会和 Flutter 的 layout 抢主线程。动画期间改 size 等于每帧触发布局约束求解。

5. 剪裁、透明、截屏:fallback 的真实原因

TLHC 看起来最好,但遇到这些绘制属性,Engine 可能静默 fallback:

  • 对 Platform View 做 ClipRRect / Opacity / Transform 3D / ShaderMask
  • 祖先 RepaintBoundary 要求和原生像素一起缓存
  • 需要读回像素的截图 OffsetLayer.toImage

fallback 到 HC 或软件读回后,你看见的是「这个页面有时掉帧」。DevTools 里 Raster 变长,但 Dart 火焰图是干净的。排障要看平台侧是否在用 overlay,而不是再减 Widget。

toImage 一张含 WebView 的图,可能走 GPU readback,在主线程或 Raster 上停几毫秒到几十毫秒。不要在动画里截。

6. 设计取舍

  • 嵌原生而不是重写 WebView/地图: 换来生态和系统能力。代价是渲染器不再独占,所有「Flutter 很顺」的假设在这块矩形上作废。
  • HC 保真 View,TLHC 保帧率: 输入法、无障碍、系统截屏 vs 单 swapchain。没有全赢的方案,所以 Engine 按能力和绘制属性动态选。
  • 触摸交给原生内部: WebView 才能滚。代价是外层 Flutter 滚动要自己让路。
  • 每 View 一个 id: 多 Engine 时 id 不能当进程全局。插件工厂必须按 engine 注册。

产品策略比技术策略更重要:能用 InAppWebView 的替代方案(自定义 Flutter 浏览器控件)通常仍更贵;能用 Flutter 地图瓦片代替 Google Map 原生 View,帧率会回到你能优化的世界。把 Platform View 当「少数例外」而不是「混排常规手段」。

7. 排障

  1. 只有嵌了 WebView 的页掉帧: 先确认当前是 VD / HC / TLHC(日志和 debug flag)。HC 就减 overlay 上的透明和圆角动画;TLHC 就减 size 动画和同时存在的实例数。
  2. 键盘弹不出 / 焦点乱跳: 怀疑 VD 或纹理路径的 IME 桥。试强制 HC。检查 resizeToAvoidBottomInset 和原生 windowSoftInputMode 是否双重处理。
  3. 外层列表不能滑: 原生 eager claim 了。改 gesture policy,或在 WebView 顶部留一段纯 Flutter 拖拽区。
  4. 缝、闪、黑块一帧: fence 没齐或纹理被重建。滚动中不要改 Platform View 的像素大小;用 transform 缩放(清楚代价)或固定 size。
  5. 内存直线涨: dump 原生 View 数量,不是 Dart ImageCache。Android Studio 的 View hierarchy 里能看见没 detach 的 WebView。
  6. 截图是黑的: 纹理/别的进程缓冲不允许 readback。改 HC 或系统截屏 API。
  7. iOS 还行 Android 不行: 合成路径不同。按上表分开治,不要写一套「优化」两端共用。

8. 工程约束清单(不是套话,是不变量的推论)

  • 同时存活的 Platform View 设硬上限。
  • 动画改 offset / opacity 可以;改 size 不行。
  • 不要把 Platform View 放进每帧重建的父级(无 Key 的列表项)。
  • 手势策略写进组件文档:谁滚、谁点。
  • 销毁路径要测:快速进出页面不应残留 Surface。

9. 和 Texture Widget 的差别

Texture(相机插件常用)不是 Platform View。它没有原生 View 层级,只有一个纹理 id,Flutter 当 TextureLayer 画。没有 IME,没有原生命中测试,手势全是 Flutter 的。能用 Texture 就不要升级成 Platform View。相机预览、外接播放器解码帧,Texture 是正确档位;需要 DOM、需要系统输入法,才升档。

10. 平台 API 怎么选:AndroidView、UiKitView、Texture

Framework 给你的入口已经暗示了汇合路径:

  • AndroidView:老路径,容易落到 VD 或 HC,兼容旧机,性能上限低。
  • AndroidViewSurface / PlatformViewLink + AndroidViewController:新路径,优先 TLHC,失败才 HC。
  • UiKitView:iOS 把 UIView 插进层级,接近 HC。UiKitView.viewType 对应原生工厂。
  • HtmlElementView:Web 上是 DOM overlay,又一套合成器(浏览器)。不要用移动端的 TLHC 经验套 Web。
  • Texture + TextureRegistry:没有原生 View。相机、播放器解码帧走这里。

创建参数走 Channel 的 codec。creationParams 太大(把整份 HTML 当参数)会在主线程编码。WebView 用 url 或后续 loadHtml,不要把 2MB HTML 塞进创建参数。

PlatformViewLink(
  viewType: 'app.map',
  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: params.viewType,
      layoutDirection: TextDirection.ltr,
      creationParams: {'lat': 39.9, 'lng': 116.4},
      creationParamsCodec: const StandardMessageCodec(),
    )
      ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
      ..create();
  },
)

gestureRecognizers 空集合:Flutter 父级可以抢手势。放入 EagerGestureRecognizer 的 factory:Platform View 在 down 时 claim。这是嵌列表里的地图「该谁滚」的正式开关,不是 IgnorePointer。

iOS 的 gestureRecognizersBlockingPolicy 有 eager 和 waitUntilTouchesEnded。后者让 Flutter 有机会先赢,WebView 内部滚动会变钝。产品上选一边写进组件注释。

PlatformViewHitTestBehavior.transparent 让 HitTest 穿透到后面的 Flutter 控件。看起来像点穿,原生 View 也可能收不到触摸。和 Listener 一样:资格层的决定,不是竞技场。

11. 合成器、OEM、以及「同一页有时卡有时不」

Android 的 Overlay 数量是硬件/OEM 限制。状态栏、导航栏、SurfaceView 播放器、Flutter 拆出来的上下两层 HC Surface、弹出来的 Dialog,都在抢 Overlay。超了就掉到 GPU composition,整页从 60 掉到 40,Dart 火焰图干净。这就是「加了个 WebView 之后动画也卡,但 WebView 不在动画里」——合成器全局变贵。

排查:adb shell dumpsys SurfaceFlinger 看 composition type。DevTools 看不见 SurfaceFlinger。TLHC 的价值在这里:系统只看见一个 Flutter Surface,Overlay 压力回到开 Flutter 之前。

强制 fallback 的绘制属性要当成「性能开关」:

  • 祖先 Opacity < 1
  • ClipRRect 且抗锯齿
  • Transform 带 perspective
  • ColorFiltered
  • 被 RepaintBoundary 要求一起缓存

能把这些从 Platform View 的祖先上拿掉,就拿掉。圆角切到原生 View 自己的 outline / cornerRadius,不要在 Flutter 侧 clip 一块纹理再 fallback 成 HC。

iOS 没有 VD 这段历史,但 UIView 和 CAMetalLayer(Flutter)叠层同样有 offscreen 渲染。UIVisualEffectView 和 Flutter blur 叠在一起是双重离屏。平台 View 上再套 Flutter BackdropFilter,两端都付费。

WebView 硬件加速在 Android 上和 Flutter 的 GPU 上下文争驱动资源。后台 WebView 仍保活一页游戏或视频,等于第二份 Raster。生命周期里 onPause 必须传到 WebView,不只传到 Flutter paused。

创建与销毁的时序:第一帧黑块从哪来

onCreatePlatformView 是异步的:原生 View 可能在下一帧甚至更晚才有第一块像素。Flutter 侧已经按 layout 出的矩形合成,纹理还是空的,于是黑块或透明洞。产品上要占位:同尺寸的 Flutter 骨架、fade-in。不要用「加大延迟再显示」赌原生速度。

销毁时序相反:Element 先 unmount,Dart 侧 Layer 摘掉,原生 View 可能还在主线程排队 removeView。快速滑动列表,创建/销毁会在主线程排成队,ANR 或掉帧发生在 Platform Runner,FrameTiming 的 build 仍健康。列表里 Platform View 必须虚拟化且 创建要节流。同时 create 三个 WebView 是主线程自杀。

PlatformViewCreatedCallback 之后才能 invokeMethod 和这块 view 说话。之前发的消息会 MissingPlugin 或打到错误 id。把「viewId 已创建」当成状态机的一档,不要在 initState 立刻 invoke。

输入法、截屏、无障碍的三处断裂

IME:HC 上 WebView 是真 View,输入法通常正常。TLHC / 纹理路径要 Engine 把焦点和 TextInput 通道接过去。双重处理(Flutter resizeToAvoidBottomInset + 原生 adjustResize)会导致键盘高度算两次,WebView 被顶出屏幕。只留一边。

截屏:系统截屏能抓到 HC 的真 View;抓纹理路径取决于是否在同一合成树。应用内 RenderRepaintBoundary.toImage 经常把 Platform View 画成黑。需要分享卡片就不要把 WebView 放进截图区域,或用原生截屏 API。

无障碍:Flutter 语义树在 Platform View 矩形上通常一个占位节点。TalkBack 焦点进 WebView 后,由原生树接手。占位节点的 label 应说明「网页」而不是空。空节点会让焦点「吞掉」这块区域又读不出内容。

混合手势的最小状态机

推荐显式三态,而不是堆识别器:

  1. undecided:down,两边都不滚动。
  2. flutter:过 slop 且方向是外层主轴,cancel 原生。
  3. platform:命中原生可滚子控件或方向是内部主轴,Flutter 认输。

用 PlatformViewGestureMixin / eager 实现 3,用空 gestureRecognizers 实现 2 的倾向。业务上「地图页外层不能滚」直接 eager,不要做三态。状态机是给 Feed 里嵌小地图这种冲突点用的。写下来,测试用「先竖滑再横滑」各一次,比看日志快。

尺寸协议:物理像素还是逻辑像素

Platform View 的 size 从 Flutter layout 来,是逻辑像素。Embedder 乘 dpr 变成物理像素去设 View 的 layout 或纹理尺寸。自己在原生按逻辑像素建纹理,图会糊;按物理像素但 Flutter 侧用逻辑当物理,图会过大且显存炸。dpr 改变(折叠屏、外接屏)必须重建纹理。动画里 dpr 不变,不要每帧 setLayoutParams。

sizedByParent 的 RenderBox 用约束最大尺寸当 Platform View 尺寸。父级给 unbounded 约束会失败——AndroidView 不能放进未约束的 ListView 横向里而不给宽度。错误信息是盒子约束,不是混合合成。先修 layout 协议。

线程亲和:WebView 只能在主线程

Android WebView 构造必须主线程,且和进程的 WebView 数据目录绑定。多进程(Application.onCreate 里非主进程)构造 WebView 会炸。Flutter 插件的后台 isolate 里 不要 间接触发 WebView 创建。所有 Platform View 工厂跑在 Platform Runner,这是硬亲和。

iOS WKWebView 类似,必须主线程。它的进程是独立 web content process,像素通过 IOSurface 过来。Flutter 这边看到的延迟包含跨进程。责怪 Flutter 合成之前,先量 WebView 自己的首画时间。

何时用截图代替真 Platform View

长列表里每项一个小地图:真 Platform View 会毁滚动。改成静态瓦片图(Texture 或普通 Image),点进去才开真地图。这是架构取舍,不是渲染优化。相机贴纸预览可以用 Texture;贴纸编辑条用 Flutter。能把「活的原生」从滚动热路径拿掉,比调 HC/TLHC 收益大一个数量级。

图层调试:看见合成路径

Android:debugHybridComposition / 引擎日志里会打当前用的 composition 类型。自己在 Debug 用半透明色标出 Platform View 矩形,确认 layout 是否比视觉大(点不中或挡住邻居)。FlutterInspector 的 select widget 对 Platform View 内部无效,这是预期——内部不是 Flutter。

iOS:Xcode View Debugger 能看见 FlutterView 层级里插入的 UIView。若你以为是纹理模式却看见真 UIView,就是 HC。以 View Debugger 为准,不以感觉为准。

内存泄漏的典型对象图

泄漏链常见:AndroidViewController → PlatformView → WebView → 页面上下文 → Activity。Flutter Element 已经 unmount,但这条 Java 链还在。用 Android Studio Memory Profiler 看 WebView 实例数是否随打开次数线性涨。涨就是没 destroy。dispose 里 controller.dispose() 必须被调用;State.dispose 里漏掉、或异步 create 完成后 State 已 dispose,都要处理:create 回调先看 mounted。

iOS 用 Instruments Leaks 看 WKWebView。FlutterViewController 自己被原生导航强引用,Engine 不 destroy,View 也不放——这是容器问题,不是 Dart 问题。

与 Impeller / 图片缓存的交叉

Platform View 的纹理不进 ImageCache。但它和 Flutter 图片抢 GPU 内存。Feed 里大图 + WebView 广告同时存在,OOM 的堆栈可能在解码,根因是纹理总和。内存警告时除了 imageCache.clear,还要暂停离屏 WebView(onPause、pauseTimers)。只清 Dart 缓存是半边。

Platform View 的产品原则是 配额。给每个 App 定:同时可见的真平台视图 ≤ 1;同时存活(含 offscreen keepAlive)≤ 2;列表项禁止。超过配额用截图/Texture/纯 Flutter 替代。没有配额,技术选型会在业务压力下全面退化成 HC+WebView 瀑布流,帧率和内存一起崩。配额写进设计评审,和「这页要嵌官方地图」一起讨论:地图是否必须可交互?不可交互就瓦片图。

手势配额同样:一页里只允许一个「eager 吃掉手指」的区域。两块 eager(地图+画布)中间的 Flutter 按钮会被饿死。eager 是稀缺资源。

版本差异要进 QA 矩阵:Android 9 VD fallback、Android 10+ TLHC、OEM 的 WebView 内核、iOS 的 WKWebView 进程崩溃(content process 死了,矩形空着,Flutter 还在跑)。WebView 崩溃不是 Flutter 崩溃,不要当成 Engine bug 升级 Flutter。监听原生 WebView 崩溃回调,显示「重新加载」而不是白块。

最后,Platform View 是 Flutter「自绘」叙事的例外,例外就应该 显眼。代码里用 package:hybrid_view 之类专用封装,禁止满项目直接 AndroidView。封装里集中:gesture 策略、创建节流、dispose、占位骨架、IME 策略。新业务只调封装。例外被集中,成本才可观测;例外被复制粘贴,成本会在三个迭代后以「Flutter 卡」的名义出现在周会上。那时再解释 TLHC,已经晚了。

Platform View 的测试策略和纯 Flutter 不同。黄金截图在 HC 下含原生像素,在 TLHC 下也可能含,但 CI 模拟器的 WebView 渲染不确定,像素测试会闪。对含 Platform View 的页,CI 只测 Flutter 部分几何和「view 已创建」回调,视觉回归放真机农场并允许 WebView 区域 mask。不 mask 会天天红。

无障碍测试同样 mask 内部:只断言占位节点存在且 label 非空。内部用原生测试工具。两端都绿才算这块混合区域完成。只测 Flutter 语义会漏 WebView 里没 label 的按钮。

安全:WebView 是 XSS 和文件域的洞。Flutter 包一层不会让 javascript: 变安全。URL 白名单、JS bridge 方法白名单,仍按原生 WebView 纪律。Channel 暴露给 JS 的接口要当公开 API,鉴权、限流。混合架构把攻击面从 Dart 扩到了 Web。这不是渲染问题,但常和 Platform View 同一模块出现,必须在同一评审里问。

电量:后台 WebView 播放、定时器、定位。onPause 传到 WebView,onResume 再开。Flutter paused 生命周期要接到封装层。漏接的表现为「杀不掉的广告 WebView 耗电」,用户怪 Flutter。封装层接 WidgetsBindingObserver 或原生 Activity 回调,两边只接一处,避免 pause 两次。

嵌入地图时还有定位权限、样式夜间模式、与 Flutter 主题不同步等问题,这些不是合成路径,但会表现为「混合页很卡」——夜间模式切换重建地图纹理,等于一次 size 变化。主题切换要节流,或让地图用自己的暗色而不跟 Flutter 每帧 Theme 动画。Theme 动画包着 Platform View 改色,是 HC fallback 和纹理重建的双重打击。能静态切就静态切。

广告 SDK 的 View 往往内部再开 WebView、再开播放器,成本是黑盒。配额上应把一块广告 View 按「≥1 个 WebView」计价,而不是按 1 计价。测 PSS 增量,用实测填配额,不要用 SDK 宣传材料。

旋转:Activity 重建若不保留 Engine,Platform View 全毁重建,用户看见闪。configChanges 或 Engine 保留策略要和 Group 一起设计。旋转时只换 Surface,不换 View id,WebView 才能保住会话。这是生命周期和混合视图的接缝,两篇文章对上才能做对。

混合页的性能预算应单独列:允许的 Platform View 个数、允许的合成模式、滚动中是否允许存在。超预算的稿子在设计阶段打回。预算数字来自真机:在目标低端机上嵌一个全屏 WebView 后 FPS 掉多少。掉到不可接受,这块业务就不要嵌,或改外链浏览器。用数据挡需求,比用「Flutter 不适合」这种空话挡需求有效。

开发时强制 Debug 横幅显示当前 composition 类型和 view 数量。类型一变(TLHC→HC)横幅变色。测试看到变色就提单,附带当时的祖先 Widget(是否 Opacity)。这比用户反馈「有时候卡」更接近根因。横幅实现十行,价值是把例外路径可视化。例外一旦看不见,就会繁殖。

混合栈导航里,Platform View 页的截图用于缩略最近任务。Android 最近任务截的是系统合成结果,HC 下含 WebView,纹理路径下也可能含。若你用 Flutter 自己截图当缩略图,黑块会出现在多任务界面。用系统截屏,或接受 Flutter 截图不含原生。产品选一个,不要两个都做再叠。多任务界面的黑块会被当成崩溃。其实是截图路径选错。把选择写进封装,业务无感。

混合视图的发布清单加三条:低端机滚动 FPS、IME 弹出、TalkBack 进入离开。三条不过不发。比再讨论一代 TLHC 更接近用户。用户不关心纹理从哪来,关心能不能滑、能不能打字、能不能听。你关心纹理,是为了这三件。三件用真机测,合成路径用日志测。两套测试一起绿,这一页的例外路径才算被驯服。驯服不是消灭。消灭是能不用就不用。能不用,配额就是零,那是最好的优化。

把 Platform View 放回整个专栏的位置:它是渲染独占假设的例外,是手势竞技场的边界,是语义树的占位,是 EngineGroup 里特别贵的可变成本。四篇文章在这块矩形上相交。相交意味着修一个问题要先问在哪条交线。掉帧问合成路径,点不动问手势策略,读不出问占位节点,内存涨问同时存活数。交线问对了,改动才落在封装里正确的开关上。问错了,会在 Dart 里减 Widget 指望 WebView 变快。那不会快。快来自少嵌、小嵌、固定尺寸嵌。封装存在,是为了让「少、小、固定」成为默认值,让例外成为显式参数。默认值比开关重要。开关会被打开;默认值会在忘记的时候保护你。

封装的默认值建议写成:gesture 不 eager、size 动画禁止、同时创建节流 1、dispose 必达、占位骨架必有。业务要改其中任何一项,必须在 PR 里写原因。原因会被未来的掉帧单引用。引用链让例外可追溯。可追溯,配额才不是墙上的纸。纸会被撕。引用链撕不掉,因为它在 git 里。git 比墙硬,和类型系统一样硬。混合视图靠这两硬活下去。

少、小、固定,加上默认不 eager,四条已经够用。够用就停。再加第三代合成名词进业务周报,帮不了 FPS。FPS 认像素和 Surface 数量。数量在配额里。配额在评审里。评审过了,这一页才能嵌。没过,外链或截图。停在这里,是这篇最贵的结论。

嵌得少,才能把独占 GPU 的主路径留给真正的 Flutter 帧。

12. 小结

混合视图的原理是「两套渲染器的像素如何汇合、输入如何让路」。VD、HC、TLHC 是三条汇合路径,不是三个 Widget 风格。选路径就是选同步点、合成次数和 IME 保真度。例外路径的成本无法在 Dart 侧用 const 优化掉。把 Platform View 用少、用小、用固定尺寸,Flutter 的那条独占 GPU 的主路径才能继续成立。