说说小程序实现原理

127 阅读13分钟

前言

在小程序没有出来之前,最初微信WebView逐渐成为移动web重要入口,但是移动网页在微信内传播体验不良能力不强

微信发布了一整套网页开发工具包,称之为 JS-SDK,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。但 JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题,这其中的原因大概能概括为这三个点:白屏问题(受限于设备性能和网络速度)、页面切换的生硬和点击的迟滞感。

为了解决这些问题,微信团队面临的问题是如何设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。这个问题是之前的 JS-SDK 所处理不了的,需要一个全新的系统来完成,它需要使得所有的开发者都能做到

  • 快速的加载。
  • 更强大的能力。
  • 原生的体验。
  • 易用且安全的微信数据开放。
  • 高效和简单的开发。

确定宿主环境

微信小程序的宿主环境为微信客户端,它是依赖于微信客户端上运行的,并且跟小程序基础库版本有重大关联关系。

我们可以把 微信客户端 以及 小程序基础库 简称为微信小程序的宿主环境。

微信小程序可以调用宿主环境提供的微信客户端的能力,可以完成许多普通网页无法完成的功能,这就使得小程序比普通网页拥有更多的能力。小程序会运行在不同版本(不同的微信客户端+不同基础库)的宿主环境下,因此针对各个版本的宿主环境做程序上的兼容也是在所难免的。

确定小程序渲染界面技术选型

一般来说,渲染界面的技术有三种:

  • 用纯客户端原生技术来渲染
  • 用纯 Web 技术来渲染
  • 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染

小程序团队通过以下几个方面分析,考虑采用哪种技术方案

  • 开发门槛:

    Web 门槛低,Native 也有像 RN 这样的框架支持

  • 体验:

    Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验

  • 版本更新:

    Web 支持在线更新,Native 则需要打包到微信一起审核发布

  • 管控和安全:

    Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险

最终决定

❌纯客户端原生技术

无法动态打包,动态下发。由于小程序的宿主环境是微信,如果用纯客户端原生技术来编写小程序,那么小程序代码每次都需要与微信代码一起发版,这种方式肯定是不行的。所以需要像web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。

❌纯 Web 技术

如果用纯web技术来渲染小程序,在一些复杂的交互上可能会面临一些性能问题,这是因为在web技术中,UI渲染跟JavaScript的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。

✅Hybrid 技术

最终采用了两者结合起来的Hybrid 技术来渲染小程序,可以用一种近似web的方式来开发,并且可以实现在线更新代码,同时引入组件也有以下好处:

  • 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力
  • 体验更好,同时也减轻 WebView 的渲染工作
  • 绕过 setData、数据通信和重渲染流程,使渲染性能更好
  • 用客户端原生渲染内置一些复杂组件,可以提供更好的性能

同时针对管控和安全的问题

解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。

微信小程序团队给出:可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。

小程序架构

在选型之后,由于渲染和逻辑不再同一个浏览器执行,因此小程序的将运行环境分成渲染层和逻辑层,一个在纯JS环境中,一个通过WebView渲染。

  • 渲染层/视图层(WebView

    主要由 WebView 进行渲染,一个小程序可以存在多个界面,所以渲染层可能存在多个 WebView 线程。

    WXML 模板和 WXSS 样式工作在渲染层。

  • 逻辑层(JSCore

    逻辑层采用 JSCore 线程运行 JS 脚本。逻辑层主要用来逻辑处理、数据请求、接口调用等。

    JS 脚本工作在逻辑层。

  • 系统层(JSBridge

    视图层和逻辑层之间的沟通则需要借助 系统层(WeixinJsBridage) 进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务逻辑处理。

    页面渲染大致过程为:把项目进行编译会把 WXML 转化成对应的 JS 对象(Virtual DOM),在逻辑层发生数据变化的时候,我们会通过 setData() 方法把数据从逻辑层传递到视图层,视图层在接收到数据后,会内部进行差异对比,把差异应用在原来的 Dom 树上,再正确的渲染出 UI 界面,完成页面的渲染过程。用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。

image.png

通信机制

首先小程序在渲染层,宿主环境会把wxml转化成对应的JS对象

  • 在逻辑层发生数据变更的时候

    通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的视图

image.png

  • 当视图存在交互的时候

    例如用户点击你界面上某个按钮,这类反馈应该通知给开发者的逻辑层,需要将对应的处理状态呈现给用户对于事件的分发处理,微信进行了特殊的处理,将所有的事件拦截后,丢到逻辑层交给JavaScript进行处理。

image.png 由于小程序是基于双线程的,也就是任何在视图层和逻辑层之间的数据传递都是线程间的通信,会有一定的延时,因此在小程序中,页面更新成了异步操作。异步会使得各部分的运行时序变得复杂一些,比如在渲染首屏的时候,逻辑层与渲染层会同时开始初始化工作,但是渲染层需要有逻辑层的数据才能把界面渲染出来,如果渲染层初始化工作较快完成,就要等逻辑层的指令才能进行下一步工作

因此逻辑层与渲染层需要有一定的机制保证时序正确,在每个小程序页面的生命周期中,存在着若干次页面数据通信

image.png

运行机制

小程序启动会有两种情况

  • 「冷启动」

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

  • 「热启动」

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

  • 小程序没有重启的概念
  • 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁
  • 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁。这也就为什么一旦页面内存溢出,页面会奔溃的本质原因了。

总之

开发者在后台发布新版本之后,无法立刻影响到所有现网用户,但最差情况下,也在发布之后 24 小时之内下发新版本信息到用户

每次冷启动时,都会检查是否有更新版本,如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上

更新机制

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

const updateManager = wx.getUpdateManager()

updateManager.onCheckForUpdate(function (res) {
  // 请求完新版本信息的回调
  console.log(res.hasUpdate)
})

updateManager.onUpdateReady(function () {
  wx.showModal({
    title: '更新提示',
    content: '新版本已经准备好,是否重启应用?',
    success(res) {
      if (res.confirm) {
        // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
        updateManager.applyUpdate()
      }
    }
  })
})

updateManager.onUpdateFailed(function () {
  // 新版本下载失败
})

性能优化

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

精简代码,降低WXML结构和JS代码的复杂性

当一个页面 DOM 结构复杂并且非常多的时候,这必定带来页面显示不及时,页面卡顿,甚至可能会出现页面奔溃的情况,这其中的原因可想而知,是过于 DOM 绘制、计算都是需要时间的,这将使得线程过渡的工作,带来客户端内存占用上升,从而触发系统回收小程序页面。

合理使用setData调用,减少setData次数和数据量

  • 频繁调用setData()

    频繁调用 setData(),这个问题相信已经是很常见的,比如在定时器中调用、在监听页面滚动的钩子中调用,这些场景很容易就会引起小程序的性能问题,容易出现页面卡顿、页面数据更新不及时的情况。

    小程序是基于双线程的,那就意味着任何在视图层和逻辑层之间的数据传递都是线程间的通信,频繁的去调用 setData(),会使得线程之间一直处于忙碌状态,逻辑层通知到视图层耗时就会上升,视图层收到消息的时候可能已经距离发出的时间超过一定时间了,渲染页面就不够及时了。

  • 庞大的数据量去调用setData()

    传输的数据需要转换成转换为字符串的形式传递,且通过 JS 脚本的形式去执行,当数据量大时,执行脚本的编译执行时间也会上涨,占用线程。

必要时使用分包优化

因小程序有体积和资源加载限制,小程序平台提供了分包方式,优化小程序的下载和启动速度。在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,会把对应分包自动下载下来,下载完成后再进行展示。

知识支撑

渲染界面技术

一般来说,渲染界面的技术有三种

  • 用纯客户端原生技术来渲染

    缺点:无法动态打包,动态下发。

  • 用纯 Web 技术来渲染

    缺点:如果我们用纯 Web 技术来渲染小程序,在一些有复杂交互的页面上可能会面临一些性能问题,这是因为在 Web 技术中,UI渲染跟 JavaScript 的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。

  • 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染

什么是webview

WebView 是移动端(手机、IPad)提供的运行 JavaScript 的环境,是系统渲染 Web 网页的一个控件,可与页面 JavaScript 交互,实现 APP 与 Web 的混合开发,WebView 渲染 Web 页面需要强大的渲染内核支持,这其中 Android 与 IOS 系统的内核又有所不一样。

setData 工作原理

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

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

常见的 setData 操作错误

  • 频繁的去 setData

    在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

  • 每次 setData 都传递大量新数据

    由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。


最后一句
学习心得!若有不正,还望斧正。希望掘友们不要吝啬对我的建议。