# 微信小程序运行原理

微信小程序介于 web 与 原生app 之间的混合app,具备丰富的调用手机各种功能的接口,同时又具备灵活性,跨平台

# 小程序与普通网页开发的区别

  • 网页开发中,渲染线程JS线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,两者可以直接进行通信

  • 小程序的逻辑层和渲染层是分开的,分别运行在不同的线程中。不同直接进行通信

# 小程序目录结

project
├── pages
|   ├── index
|   |   ├── index.json  index 页面配置
|   |   ├── index.js    index 页面逻辑
|   |   ├── index.wxml  index 页面结构
|   |   └── index.wxss  index 页面样式表
|   └── log
|       ├── log.json    log 页面配置
|       ├── log.wxml    log 页面逻辑
|       ├── log.js      log 页面结构
|       └── log.wxss    log 页面样式表
├── app.js              小程序逻辑
├── app.json            小程序公共设置
└── app.wxss            小程序公共样式表

# 为什么小程序快

  • 安装包缓存

  • 分包加载

  • 独立渲染线程

  • Webview 预加载

  • Native组件

# 小程序启动加载

小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。

  • 热启动:假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动

  • 冷启动:指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。

# 更新机制

小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。

# 运行机制

  • 小程序没有重启的概念

  • 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁

  • 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁

# 小程序架构

微信小程序的框架包含两部分:View视图层(可能存在多个)、App Service逻辑层(一个)

  • View层用来渲染页面结构, 使用WebView渲染

  • AppService层用来逻辑处理、数据请求、接口调用,它们在两个线程里运行,逻辑层使用JSCore运行

视图层和逻辑层通过系统层的 WeixinJsBridage 进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理

这张图展示了视图层、逻辑层之间通信方式,以及JsBridge起到纽带的作用。我们可以做以下几点总结:

  • 视图层和逻辑层分开在两个线程中运行;

  • 视图层、逻辑层通过事件完成通信;

  • JsBridge一方面传递基础功能,另一方面做视图层和逻辑层的数据传递工作;

# 执行环境

微信小程序运行在三端:iOS、Android 和 用于调试的开发者工具

三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:

  • 在 iOS 上,小程序的 javascript 代码是运行在 JavaScriptCore 中,是由 WKWebView 来渲染的,环境有 iOS8、iOS9、iOS10

  • 在 Android 上,小程序的 javascript 代码是通过 X5 JSCore来解析,是由 X5 基于 Mobile Chrome 53/57 内核来渲染的

  • 在 开发工具上, 小程序的 javascript 代码是运行在 nwjs 中,是由 Chrome Webview 来渲染的

# 视图层和逻辑层

视图层(不包括Native组件)在webview组件中渲染;逻辑层的js代码则通过js容器来执行。js代码执行过程中可以通过JsBridge调起微信、系统的能力,也可以将数据通过setData()函数传递给视图层做渲染。视图层在渲染和交互过程中可以通过事件与客户端进行通信

视图层向逻辑层的通信由事件来完成,而逻辑层数据向视图层发送渲染指令是通过 Page 的 setData 函数。下面我们详细说明他们的细节原理

这张生命周期图非常详尽的描述了一个页面从创建入栈、数据交互、销毁出栈的整个过程。在页面渲染和使用过程中会出现大量的事件,而这些事件会被JsBridge捕获到,并传递给逻辑层处理,主要包括:生命周期事件、UI事件

生命周期事件

视图进程在完成阶段性工作后,需要向逻辑层同步其当前状态以便逻辑层做出应对策略。主要包括:onLoadonReadyonShowonHideonUnloadonPullDownRefreshonReachBottomonShareAppMessage

UI事件

视图层向逻辑层的通信方式

这类事件绑定在组件上,触发则可以将用户的行为反馈到逻辑层对应的注册函数,如 bindtapbindinputbindconfirmbindfocusbindsubmitbindchangebindlinechange

Page.prototype.setData()

逻辑层向视图层发送渲染指令的方式

setData 函数用于将数据从逻辑层发送到视图层,同时改变对应的 this.data 的值。
注意:data 将会以 JSON 的形式由逻辑层传至渲染层,所以其数据必须是可以转成 JSON 的格式:字符串,数字,布尔值,对象,数组

从性能角度考虑,UI渲染是比较耗时的,一方面尽量减少页面渲染操作,另一方面其会对 data 的内容做深层次 diff,尽量控制 data 的大小和数据结构的深度(官方限制data数据量最大为1024K)。

# 小程序基础库

小程序基础库就同 JS-SDK 类似,为当前应用调起手机系统能力和微信能力,两者的不同点在于:

  • 支持的API:小程序基础库对系统能力和微信能力做了更全面的封装,包括网络请求、存储等,而网页版JS-SDK相应的功能直接调用window能力

  • 引入方式:小程序基础库直接集成到微信的不同版本中,JS-SDK是以一个js文件的形式被引入项目里;

# 小程序基础库与微信客户端之间的关系

小程序的能力需要微信客户端来支撑,每一个基础库都只能在对应的客户端版本上运行,高版本的基础库无法兼容低版本的微信客户端

官方的这种说法存在一些问题。现在基础库版本和客户端版本并不是一一对应关系。客户端可以主动升级小程序基础库版本达到灰度上线新版的目的,所以必然存在一个客户端版本对应多个基础库版本的情况

小程序基础库更新时机

为了避免新版本的基础库给线上小程序带来未知的影响,微信客户端都是携带 上一个稳定版 的基础库发布的。
在新版本客户端发布后,我们再通过后台灰度新版本基础库,灰度时长一般为 12 小时,在灰度结束后,用户设备上才会有新版本的基础库

以微信 6.5.8 为例,客户端在发布时携带的是 1.1.1 基础库(6.5.7上已全量的稳定版)发布,在 6.5.8 发布后,我们再通过后台灰度 1.2.0 基础库。

“细思恐极”,如果我们已经完成一台装有 6.5.4 版本微信的Oppo手机对小程序的兼容测试,很有可能过几天这台Oppo手机将小程序基础库更新到新版本导致小程序不可用。建议:了解产品的用户手机微信版本分布,确定回归覆盖范围,完成回归测试。

# 小程序开发经验

小程序存在的问题

  • 小程序仍然使用WebView渲染,并非原生渲染

  • 需要独立开发,不能在非微信环境运行

  • 开发者不可以扩展新组件

  • WXSS中无法使用本地(图片、字体等)

  • WXSS转化成js 而不是css。

  • WXSS不支持级联选择器

小程序的优点

  • 提前新建 WebView,准备新页面渲染

  • 使用 Virtual DOM,进行局部更新

  • 全部使用 https,确保传输中安全

  • 加入 rpx单位,隔离设备尺寸,方便开发

# 优化建议(官方建议)

setData 工作原理

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块, 并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。 即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境

evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的

常见的 setData 操作错误

  • 频繁的去 setData

    在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:

    • Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;

      渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

每次 setData 都传递大量新数据

由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程

  • 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

# 图片资源

  • 目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面

  • 在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收。

# 代码包大小的优化

开发者在实现业务逻辑同时也有必要尽量减少代码包的大小,因为代码包大小直接影响到下载速度,从而影响用户的首次打开体验。除了代码自身的重构优化外,还可以从这两方面着手优化代码大小:

  • 分包加载

    对小程序进行分包,可以优化小程序首次启动的下载时间

  • 清理没有使用到的代码和资源

目前小程序打包是会将工程下所有文件都打入代码包内,也就是说,这些没有被实际使用到的库文件和资源也会被打入到代码包里,从而影响到整体代码包的大小。

# 预先加载数据

微信小程序之提高应用速度小技巧 (opens new window)

原理

小程序在启动时,会直接加载所有页面逻辑代码进内存,即便 page2 可能都不会被使用。在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。 page2 甚至可以直接访问 page1 中的数据

小程序的这种机制差异正好可以更好的实现预加载。通常情况下,我们习惯将数据拉取写在 onLoad 事件中。但是小程序的 page1 跳转到 page2,到 page2 的 onLoad 是存在一个 300ms ~ 400ms 的延时的。如下图:

pageA跳到pageB的时候,在pageB使用onNavigate生命周期来在页面B即将被加载时运行

微信小程序运行流程看这篇就够了 (opens new window)
深入解读-微信小程序SDK (opens new window)
浅谈小程序运行机制 (opens new window)