微信小程序『同层渲染』技术是怎么回事?

2,072 阅读5分钟

微信小程序开始推广自绘引擎( 微信 Skyline 渲染引擎类似 Flutterr 的 SkyEngine),故本文所涉内容终将被淹没在技术进步的历史长河中,且看且珍惜!

前言

Webview 资源离线化之后不需要从后端拉取资源,省去了资源加载耗时减少等待时间,用户体验得到了明显提升,但受限于 Webview 的天生限制在某些场景下仍然能感受到与原生明显的体验落差。比如输入框弹出键盘时的页面卡顿、图片较多的页面内存占用过从后台回来前台时页面被重新加载、WebP 图片支持碎片化、图片不能跨页共享内存、视频播放组件体验不佳等问题。在 Webview 本身受限的情况下,如何在体验上向原生靠近是我们不得不面对的问题。如果能将影响用户体验的关键可交互组件用原生组件进行替换可以从根本上解决体验不如原生的问题。这部分内容我称为: WebView原生化

Webview 原生化

Webview 原生化是指把 Webview 内部分占位 DOM 元素用原生组件进行替换,原生组件与原始 Webview 混合便得到了用户看到的最终界面。组件替换有两种方式,一种是在 Webview 的直接上层添加原生视图蒙层,把原生组件添加到蒙层上占位 DOM 对应的位置。另一种是将原生组件添加到 Webiview 渲染时与占位 DOM 对应的合成层(独立出来的原生视图)上,下面会对比两种方案的差异。

1631023454602-142bdbc2-be31-4274-a514-6206ff98cdfb.png

传统方案

方案简介

将原生组件添加到 Webview 上的蒙层是传统方案。思路也很简单,将 Webview 内要用原生替换的组件用占位 DOM 进行封装,占位 DOM 被展示时发送 DOM 的类型、位置、形状等其它信息给到原生。原生在拿到信息后生成对应的原生组件添加到蒙层之上。

1631025348287-e5705ee2-94ad-4021-974d-528387e0c530-2185524.png

传统方案缺陷

原生组件的渲染实际上是完全独离于 Webview 的渲染流程,甚至是在整个 Webview 的视图之外。这就也意味着:

  • 原生组件的层级是最高的:页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上;

  • 部分 CSS 样式无法应用于原生组件;

  • 原生组件无法在 scroll-view、swiper、picker-view、movable-view 中使用:因为如果开发者在可滚动的DOM区域,插入原生组件作为其子节点,由于原生组件是直接插入到webview外部的层级,与DOM之间没有关联,所以不会跟随移动也不会被裁减

1631025270754-8c9514e4-662e-41ac-a41b-a48e1814ef59-2185535.png

这也就是说当一个原生组件被添另到 Webview 上时它永远处顶层,不会被 Webview 内的弹窗覆盖,在滚动时原生组件会盖住原本应被显示的区域。

同层渲染

方案简介

原生组件与 DOM 图层在同一层级渲染的方式叫同层渲染。

WKWebview 在进行渲染操作时,会将若干个 DOM 元素混合(Composition)后渲染到原生合成视图(WKComponiitingView)上,如果能将原生合成视图与指定 DOM 进行对应那便可以将原生组件直接添加到合成视图上。

1631026198990-9f1e5ef9-f271-4e64-80e8-8b608674d29f-2185548.png

如上图中选中的图层就是由多个 DOM 合成显示在一起,只要将其中的图片部分独立出一个单独的图层渲染,把原生图片添加到独立出来的原生视图就可以实现同层渲染。其前置条件是:

当把 DOM 节点的 CSS 属性设置为 overflow: scroll (低版本需同时设置 -webkit-overflow-scrolling: touch)之后,原生 WKWebView 会为其生成一个对应的 WKChildScrollView

具体实现流程为:

  1. 小程序前端,在webview内创建一个 DOM 节点并设置其 CSS 属性为 overflow: hidden 且 -webkit-overflow-scrolling: touch;且保证其有个比当前 DOM 大的子 DOM,可添加1px的外边距解决;

  2. 前端通知客户端查找到该 DOM 节点对应的原生 WKChildScrollView 组件;

  3. 将原生组件挂载到该 WKChildScrollView 节点上作为其子 View;

  4. WebKit 内核已经处理了WKChildScrollView与对应DOM 节点之间的层级关系;

前端建创原生视图节点的 DOM 样式示例:

<div class="container cid_1" data-component-type="input" style="width: 200px; height: 40px"> 
    <div style="width: 101%; height: 101%">&nbsp;</div>
</div>

<style>     
	.container {  /* insert WKChildView in WKWebView */
		overflow: scroll; -webkit-overflow-scrolling: touch;    
 	}
 </style>

把 DOM 节点按前置条件设置之后,WebKit 引擎将图片部分独立出来并插入了一个 WKChildScrollView,只需要在视图树上找到对应的 WKChildScrollView 把原生的 UIImageView 添加到 WKChildScrollView 即可。添加后的原生视图可跟随页面一起滚动并保留了 DOM 树深度信息,不会覆盖原本应该在上层的组件。

1631066270204-9aa4886d-d214-48e0-bee6-e04c1dc23f5f-2185558.png

如何映射 DOM 到原生视图

独立出来的 WKCompositingView 及其子视图 WKChidScrollView 如何与 DOM 对应呢?通过打印 WKCompositingView 的 description 属性发现其中包含了类型(div)及 class 信息, 可以通过给每个需要原生化的组件一个唯一 class 名称,前端通过收集 DOM 属性,把 class 信息传递给原生,原生遍历查找视图树即可。

如何在原生视图树中查找 「DOM」

WKWebview 渲染出的视图树可能非常复杂层级较多,如果每次都去遍历视图树在快递滚动的场景可能会出现性能问题。由于与 DOM 对应的 WKCompositingView 是唯一的,可以通过一个弱引用 Map 去记录每个 class 对应的 WKCompositingView,避免每次都去遍历整个树。

前端原生组件封装

DOM 节点与原生视图的对应关系搞定之后,需要前端封装对应「虚拟组件」。虚拟组件的作用是用来标示位置,大小,形状等信息不用于实际的渲染,渲染通过调用原生的方法(通过 Bridge)由原生处理。此方案正是微信小程序目前使用的方案,目前微信的 canvas、map、animation-view、textarea、cover-view、cover-image、camera、video、live-player、input 等组件都是由原生进行渲染。微信只提供了实现思路,当中的细节处理还需要进一步探索。