乐趣区

微信,支付宝小程序实现原理概述

微信小程序
项目结构

上图为微信小程序的项目结构,pages 下面包含了小程序中的每一个页面,每一个页面由页面结构,页面样式,页面配置和逻辑代码四部分组成。
页面结构
页面结构文件为 index.wxml,通过微信自定义的标签来写。
页面逻辑
页面逻辑通过 JavaScript 来书写。
页面样式表
类似 CSS 文件,来定义页面内元素的样式。
页面配置
页面内的权限等配置信息。
微信小程序的技术选型
小程序的定位特点是轻,快,针对这两个特点,在技术选型上,微信进行了一些考量。
渲染界面的技术
用纯客户端原生技术来渲染
缺点:无法动态打包,动态下发。
用纯 Web 技术来渲染
缺点:如果我们用纯 Web 技术来渲染小程序,在一些有复杂交互的页面上可能会面临一些性能问题,这是因为在 Web 技术中,UI 渲染跟 JavaScript 的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占 UI 渲染的资源。
介于客户端原生技术与 Web 技术之间的,互相结合各自特点的技术来渲染
从渲染底层来看,PhoneGap 与微信 JS-SDK 是类似的,它们最终都还是使用浏览器内核来渲染界面。而 RN 则不同,虽然是用 Web 相关技术来编写,同样是利用了 JavaScript 解释执行的特性,但 RN 在渲染底层是用客户端原生渲染的。我们选择类似于微信 JSSDK 这样的 Hybrid 技术,即界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的 WebView 去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个 WebView 的任务过于繁重。
微信没有选择 RN 的原因

RN 所支持的样式是 CSS 的子集,会满足不了 Web 开发者日渐增长的需求,而对 RN 的改造具有不小的成本和风险。
RN 现有能力下还存在的一些不稳定问题,比如性能、Bug 等。RN 是把渲染工作全都交由客户端原生渲染,实际上一些简单的界面元素使用 Web 技术渲染完全能胜任,并且非常稳定。
RN 存在一些不可预期的因素,比如之前出现的许可协议问题

原生组件的渲染方式
在安卓则是往 WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层,主要提供了调用(invoke)和监听(on)这两种方法。开发者插入一个原生组件,一般而言,组件运行的时候被插入到 DOM 树中,会调用客户端接口,通知客户端在哪个位置渲染一块原生界面。在后续开发者更新组件属性时,同样地,也会调用客户端提供的更新接口来更新原生界面的某些部分。
Web 渲染带来的问题与解决
提供干净纯粹的 JavaScript 执行环境
由于 JavaScript 的灵活性和浏览器的功能丰富,会导致很多不可控的隐私,因此,微信提供了一个单纯的 JS 执行环境,通过对于其中的控件也进行了自定义。因此完全采用这个沙箱环境不能有任何浏览器相关接口,只提供纯 JavaScript 的解释执行环境,那么像 HTML5 中的 ServiceWorker、WebWorker 特性就符合这样的条件,这两者都是启用另一线程来执行 JavaScript。但是考虑到小程序是一个多 WebView 的架构,每一个小程序页面都是不同的 WebView 渲染后显示的,在这个架构下我们不好去用某个 WebView 中的 ServiceWorker 去管理所有的小程序页面。得益于客户端系统有 JavaScript 的解释引擎(在 iOS 下是用内置的 JavaScriptCore 框架,在安卓则是用腾讯 x5 内核提供的 JsCore 环境),我们可以创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码,也就是我们前面一直提到的逻辑层。而界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面,那么这一层当然就是所谓的渲染层。这就是小程序双线程模型的由来。
标签自定义
为了防止标签定义带来的一些问题,微信自定义了一套标签语言,WXML,这套标签语言经过编译之后,最终会生成 Html。
渲染与逻辑的分离
上面是小程序的渲染技术的选型,在选型之后,由于渲染和逻辑不再同一个浏览器执行,一个在纯 JS 环境中,一个通过 WebView 渲染,因此小程序的运行环境分成渲染层和逻辑层,WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。
小程序的渲染层和逻辑层分别由 2 个线程管理:渲染层的界面使用了 WebView 进行渲染;逻辑层采用 JsCore 线程运行 JS 脚本。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程,这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求也经由 Native 转发,小程序的通信模型如图所示。

数据驱动视图变化
在开发 UI 界面过程中,程序需要维护很多变量状态,同时要操作对应的 UI 元素。随着界面越来越复杂,我们需要维护很多变量状态,同时要处理很多界面上的交互事件,整个程序变得越来越复杂。通常界面视图和变量状态是相关联的,如果有某种“方法”可以让状态和视图绑定在一起(状态变更时,视图也能自动变更),那我们就可以省去手动修改视图的工作。
小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把 WXML 转化成对应的 JS 对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的 setData 方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的 Dom 树上,渲染出正确的 UI 界面。

通过 setData 把 msg 数据从“Hello World”变成“Goodbye”,产生的 JS 对象对应的节点就会发生变化,此时可以对比前后两个 JS 对象得到变化的部分,然后把这个差异应用到原来的 Dom 树上,从而达到更新 UI 的目的,这就是“数据驱动”的原理。

事件的处理
UI 界面的程序需要和用户互动,例如用户可能会点击你界面上某个按钮,又或者长按某个区域,这类反馈应该通知给开发者的逻辑层,需要将对应的处理状态呈现给用户。由于 WebView 现在具备的功能只是进行渲染,因此对于事件的分发处理,微信进行了特殊的处理,将所有的事件拦截后,丢到逻辑层交给 JavaScript 进行处理。

事件的派发处理,具备事件捕获和冒泡两种机制。通过 native 传递给 JSCore,通过 JS 来响应响应的事件之后,对 Dom 进行修改,改动会体现在虚拟 Dom 上,然后再进行真实的渲染。

数据通信
小程序是基于双线程模型,那就意味着任何数据传递都是线程间的通信,也就是都会有一定的延时。这不像传统 Web 那样,当界面需要更新时,通过调用更新接口 UI 就会同步地渲染出来。在小程序架构里,这一切都会变成异步。
异步会使得各部分的运行时序变得复杂一些。比如在渲染首屏的时候,逻辑层与渲染层会同时开始初始化工作,但是渲染层需要有逻辑层的数据才能把界面渲染出来,如果渲染层初始化工作较快完成,就要等逻辑层的指令才能进行下一步工作。因此逻辑层与渲染层需要有一定的机制保证时序正确,
在每个小程序页面的生命周期中,存在着若干次页面数据通信。逻辑层向视图层发送页面数据(data 和 setData 的内容),视图层向逻辑层反馈用户事件。

通过 Json 的方式进行数据的传递,提高性能的方式就是减少交互的数据量。
缓存机制
小程序宿主环境会管理不同小程序的数据缓存,不同小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为 10MB,如果当前缓存已经达到 10MB,再通过 wx.setStorage 写入缓存会触发 fail 回调。
小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露。
由于本地缓存是存放在当前设备,用户换设备之后无法从另一个设备读取到当前设备数据,因此用户的关键信息不建议只存在本地缓存,应该把数据放到服务器端进行持久化存储。
支付宝小程序
支付宝小程序简介
支付宝小程序的实现和微信小程序的实现方式大致是相同的,因此这里主要针对两者的差异性的地方。
支付宝小程序目录结构
支付宝小程序业务架构图
在渲染引擎上面,支付宝小程序不仅提供 JavaScript+Webview 的方式,还提供 JavaScript+Native 的方式,在对性能要求较高的场景,可以选择 Native 的渲染模式,给用户更好的体验。
运行时架构
小程序编程模型是分为多个页面,每个页面有自己的 template、CSS 和 JS,实际在运行的时候,业务逻辑的 JS 代码是运行在独立的 JavaScript 引擎中,每个页面的 template 和 CSS 是运行在各自独立的 webview 里面,页面之间是通过函数 navigateTo 进行页面的切换。
每个 webview 里面的页面和公共的 JavaScript 引擎里面的逻辑的交互方式是通过消息服务,页面的一些事件都会通过这个消息通道传给 JavaScript 引擎运行环境,这个运行环境会响应这个事件,做一些 API 调用,可调到客户端支付宝小程序提供的一些能力,处理之后会把这个数据再重新发送给对应的页面渲染容器来处理,把数据和模板结合在一起来,在产生最终的用户界面。

支付宝小程序虚拟机隔离
通常的做法是在 WebView 里面运行 render 的代码,然后另起一个线程运行 serviceworker,当 serviceworker 需要更新 dom 的时候把事件和数据通过 messagechannel 发送给 render 线程来执行,当业务需要传递到 render 层数据量较大,对象较复杂时,交互的性能就会比较差,因此针对这种情况我们提出一个优化的解决方案。
该方案将原始的 JS 虚拟机实例 (即 Isolate) 重新设计成了两个部分:Global Runtime 和 Local Runtime。

Global Runtime 部分是存放共享的装置和数据,全局一个实例。
Local Runtime 是存放实例自身相关的模块和私有数据,这些不会被共享。

在新的隔离模型下,webview 里面的 v8 实例就是一个 Local Runtime,worker 线程里面的 v8 实例也是一个 Local Runtime,在 worker 层和 render 层交互时,setData 对象的会直接创建在 Shared Heap 里面,因此 render 层的 Local Runtime 可以直接读到该对象,并且用于 render 层的渲染,减少了对象的序列化和网络传输,极大的提升了启动性能和渲染性能。

首屏速度优化

由于小程序启动是受到生命周期的控制,从 onLaunch -> onLoad -> onShow -> onReady -> 用户操作 -> 离开首页这个流程,在这个过程中的任意一个环节都有可能被客观或者主观的原因打断,也就有可能导致保存的离线页面不准确,在启动的时候给用户呈现错误的页面。
所以对于首页离线缓存渲染的效果,保存页面的时机很重要,我们提供让开发者可以配置的时机,配置的时机有两个:渲染完成和离开首页前。对于渲染完成就是首页渲染完成,用户还未执行任何的操作前把页面保存下来作为离线缓存的页面。离开首页前就是指用户在首页执行了一系列的操作后,跳转到其他页面前用户看到的页面保存下来作为离线缓存的页面。

对于闪屏问题发生的场景是因为缓存页面和真实渲染的页面是分离的,是两个独立的页面,缓存页面是静态的页面,真实的页面是通过 js 动态创建的页面,所以常规的做法就是当真实页面创建完成后替换缓存的页面,这样的情况下就会发生闪屏。

针对这个问题,我们是采用虚拟 dom 来解决,在加载缓存页面的时候把缓存页面放入初始的虚拟 dom 里面,真实页面创建后产生的虚拟 dom 跟缓存页面的虚拟 dom 进行 dom diff,把变化的内容通过 patch 传给浏览器内核,渲染对应的页面,这样就可以只更新局部有变化的页面内容,避免了整个页面的更新,也保证内容的准确性和实时性。

支付宝采用 UC 浏览器内核优势
1. 图片内存:针对低端机,做了更严格的图片缓存限制,在保持性能体验的情况下,进一步限制图片缓存的使用;多个 webview 共用图片缓存池;全面支持 webp、apng 这种更节省内存和 size 的图片格式。
2. 渲染内存:Webview 在不可见的状态下,原生的内存管理没有特殊处理,UC 内核会将不可见 webview 的渲染内存释放;渲染内存的合理设置与调优,避免滚动性能的下降和占用过多内存。
3.JS 内存:更合理地处理 v8 内存 gc,在启动时延时执行 full gc,避免影响启动的耗时。
4. 峰值内存管理:系统在内存紧张时,会通知内核,UC 内核能够在系统低内存时释放非关键内存占用的模块,避免出现 oom,也避免过度释放带来的渲染黑块;在部分 oom 的情况,规避原生内核主动崩溃的逻辑,在内存极低的情况,部分功能不可用,而不是崩溃。
对我们的启示
小程序存储管理
增加小程序的存储,包括内存和磁盘,可以缓存部分数据,增加页面直出速度。同时对于磁盘的管理,按照小程序账号双重维度进行划分。
第三方业务接入能力限制
在支持第三方的接入之后,按照现有方式将会导致对于安全和第三方的行为完全不可控,可以参考微信,支付宝方式采用自定义标记语言的方式对标记语言做限制,并提供纯净的 JS 环境来进行 JS 环境的执行,WebView 只负责渲染。
首屏速度
参考支付宝方案,在加载的时候,现将老的页面呈现给用户,然后在新页面完成之后,计算差值,再进行显示。
Native 绘制结合
Native 绘制采用通过 JS 和 Native 通信的方式,将 Native 控价加入到布局的制定区域。
网络请求发送托管
网络请求等全部交由 Native 托管,更好的控制网络请求,监控网络请求。
参考文章

支付宝小程序实现
微信小程序开发文档
微信小程序原理文档

退出移动版