0%

小程序底层原理刨析

小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。从前端技术选型方案角度考虑,探寻小程序底层机制。

01:小程序底层原理

1. 从前端技术选型方案角度考虑,探寻小程序底层机制

  • 用纯客户端原生技术来渲染
  • 用纯 Web 技术来渲染
  • 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染(小程序用这种)
    • 扩展 Web 能力,比如像输出框组件(input,textarea)更好控制键盘能力
    • 体验更好,减轻WebView的渲染工作
    • 用客户端原生渲染一些复杂组件,提供更好的性能

开发者若是直接通过JS操作DOM,那么一些敏感数据就毫无安全可言,微信提供沙箱环境来运行开发者的JS代码,这个环境不能有任何浏览器相关的接口,只能通过 JS 解释执行环境,类似于 HTML5 的 ServiceWorker 启动另一个线程来执行 JS 。

这样设计的原因是为了解决管控安全问题,需要阻止开发者试用浏览器window对象,跳转页面,操作DOM,动态执行脚本的开放性接口。

2. 双线程模型与双线程通信具体流程

image

这样设计的原因是为了解决管控安全问题,需要阻止开发者试用浏览器window对象,跳转页面,操作DOM,动态执行脚本的开放性接口。


可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架, 安卓下腾讯 x5 内核提供的 JScore 环境。

这个沙箱环境提供纯 JavaScript 解释执行环境,没有任何浏览器相关接口。

这就是小程序双线程模型的由来:

  • 逻辑层:创建一个单独的线程去执行 JavaScript ,在这里执行的都是小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等
  • 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个 WebView 线程
  • JSBridge 起到架起上层开发与 Native (系统层)的桥梁,使得小程序可通过 API 使用原生功能,且部分组件为原生组件实现,从而有良好体验

image

  • 在渲染层把 WXML 转化成对应的 JS 对象。
  • 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。
  • 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。而这样一个完整的框架,离不开小程序的基础库

3. 逻辑层和渲染层的交互和通信离不开小程序的基础库Exparser

小程序基础库可以被注入到视图层和逻辑层运行,用于一下几个方面:

  • 视图层,提供各类组件来组建界面元素
  • 逻辑层,提供各类 API 来处理各种逻辑
  • 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑

小程序渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库

小程序的基础库不会被打包在某个小程序的代码包里,会提前内置到微信客户端里,这样可以:

  • 降低业务小程序的代码包大小
  • 可以单独修复基础库中的 Bug, 无需修改到业务小程序的代码包

Expareser框架

Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。

Exparser的主要特点包括以下几点:

  • 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
  • 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
    高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
  • 小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。
    内置组件

基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。

4. 小程序运行机制

小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。

小程序没有重启的概念

当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁。

5. 什么是JSBridge

JavaScript 是运行在一个单独的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于这些 Context 与原生运行环境的天然隔离,我们可以将这种情况与 RPC(Remote Procedure Call,远程过程调用)通信进行类比,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。

在 JSBridge 的设计中,可以把前端看做 RPC 的客户端,把 Native 端看做 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通信调用(Native 与 JS 通信) 和句柄解析调用。

作用:主要是给JavaScript提供调用 Native 功能的接口,让混合开发中的前端部分可以方便的使用 Native 功能(例:地址位置、蓝牙、摄像头)

而且 JSBridge 的功能不止调用 Native 功能这么简单,实际上,JSBridge 就像其名称中的 Bridge 的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是构建 Native 和非 Native 间消息通信的信道,且这个信道是双向的。

双向通信的信道:

  • JS 向 Native 发送消息:调用相关功能,通知 Native 当前 JS 的相关状态等
  • Native 向 JS 发送消息:回溯调用结果、消息推送,通知 JS 当前 Native 的状态

6. 开发中性能优化

主要的优化策略可以归纳为三点:

  • 精简代码,降低WXML结构和JS代码的复杂性;
  • 合理使用setData调用,减少setData次数和数据量;
  • 必要时使用分包优化。

1、setData 工作原理

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

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

2、常见的 setData 操作错误

频繁的去 setData在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
每次 setData 都传递大量新数据由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript
脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行
setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

总结:

分析了小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。关于小程序的底层框架设计,其实涉及到的还有很多,比如自定义组件,原生组件、性能优化等方面,都不是一点能讲完的,还要多看源码,多思考。每一个框架的诞生都有其意义,我们作为开发者能做的不只是会使用这个工具,还应理解它的设计模式。只有这样才不会被工具左右,才能走的更远!