支付宝小程序:从 Service Worker 到 V8 Worker 的技术演进之路

2024-10-24
来源:网络整理

从 到 V8

本节简单介绍一下支付宝小程序从V8开始的技术演进过程。

众所周知,支付宝小程序源码打包后主要分为两部分:

同时前端框架APPX也分为两部分(af-appx.min.js)和两部分(af-appx..min.js):

由浏览器内核提供,旨在充当Web应用程序和浏览器之间的代理服务器;在单独的上下文中运行,因此它无法访问 DOM。它运行在与驱动应用程序的主线程不同的线程中,因此不会导致阻塞。

但存在一个问题,启动和部分启动是串行的,启动后必须由部分JS发起。这是小程序的主要性能瓶颈。

为了解决串行初始化和执行带来的性能问题,小程序团队尝试使用to执行。也就是说,启动小程序时,同时new了两个,一个用来渲染部分,另一个专门用来执行部分JS脚本。但专门用一个来执行部分JS脚本无疑是“大材小用”,而且使用的资源消耗一定很高。

V8

小程序的串行初始化会影响小程序的启动性能,而且运行小程序代码不够轻量。使用专有的JS引擎来做一些工作是最好的选择,因此V8应运而生。

下图展示了小程序V8的基本结构,本文后面会详细介绍。

使用V8发动机运行有以下主要优点:

V8基础设施

本节主要介绍支付宝小程序的V8项目结构以及基于V8的小程序架构。同时,如果你对V8引擎不是很熟悉,这里有V8的简单介绍和学习资料链接。

V8 简介和入门

在介绍V8之前,我们先简单了解一下V8引擎[2]本身。如果您熟悉V8,请跳过它。

V8是一个开源项目,一个高性能引擎,用于浏览器、Node.js等项目。学习V8的门槛还是比较高的。这里我们只给出阅读本文需要了解的V8基本概念,以及官方嵌入的V8代码,并且还给出了一些学习链接。

嵌入式V8基本概念

1(隔离)

有点类似于操作系统中进程的概念。进程之间是完全隔离的。一个进程中有多个线程,进程之间不共享资源。同样的事情,两个虚拟机实例都有自己的堆栈,彼此完全隔离。

2(上下文)

在V8中,执行环境是一个执行环境,它允许您在V8实例中运行隔离的、不相关的代码。您必须为要执行的代码明确指定一个。

原因是提供了一些内置的实用函数和对象,并且可以通过JS代码修改它们。例如,如果两个完全不相关的 JS 函数以相同的方式修改一个对象,则很可能会出现意想不到的结果。

3(句柄)和垃圾收集

提供对堆内存中 JS 对象地址的引用。 V8垃圾收集器将回收不再可访问的对象所占用的堆内存空间。在垃圾收集过程中,收集器通常会移动堆内存中的对象。当收集器移动对象时,它也会将所有相应的对象更新到新的地址。

当一个对象无法被访问并且没有对它的引用时,该对象将被视为“垃圾”。收集器将继续从堆内存中删除所有被确定为“垃圾”的对象。 V8 的垃圾收集机制是其性能的关键。

保存在堆栈结构中,当调用堆栈的析构函数()时,它会同时被销毁。这些的生命周期取决于(当一个函数被调用时,相应的函数就会被创建)。当一个对象被销毁时,如果其中引用的对象无法再被访问,或者没有其他指针指向它,那么这些对象将在销毁过程中被垃圾收集器回收。入门指南中的示例使用了此方法。

是对分配在堆内存上的对象的引用,这与.但它有自己的两个功能,即与其关联的引用的生命周期管理方面。当你想要持有一个超出函数调用的时期或范围的对象的引用时,或者当引用的生命周期与C++的范围不一致时,你需要使用它。一个示例是使用引用 DOM 节点。支持弱引用,即::,当它引用的对象只剩下弱引用时,可以触发垃圾收集器的回调。

4(模板)

在 a 中,是函数和对象的模型。您可以使用将C++函数和数据结构封装在一个对象中,以便可以通过JS代码对其进行操作。例如,使用C++ DOM节点封装成JS对象,以及安装在命名空间中的函数。您可以创建一个可以在每次创作中重复使用的集合。您可以根据需要创建任意多个。然而,任何一个都只能有一个实例。

在 JS 中,函数和对象之间存在很强的二元性。在 C++ 或 Java 中创建新的对象类型通常涉及定义类。在JS中,你必须创建一个函数,并使用该函数作为构造函数来生成对象实例。 JS 对象的内部结构和功能很大程度上取决于构造它的函数。这些也体现在V8的设计上,所以V8有两种类型:

1)

一种是 JS 函数的模型。我们可以通过调用我们指定的方法来创建一个JS函数的实例。您还可以将 C++ 回调与执行时调用的 JS 函数实例关联起来。

2)

每一个都与一个相关联。它用于配置使用此函数创建的对象作为构造函数。

5(存取器)

访问器是一个 C++ 回调,当 JS 代码访问对象属性时,它会计算并返回一个值。访问器是通过方法配置的。该方法接收属性名称及其关联的回调函数,分别在 JS 读写属性时触发。

访问器的复杂性取决于您操作的数据的访问方式:

6(拦截机)

我们可以设置一个回调,以便当访问相应对象的任何属性时都会调用它。就是这样。考虑到效率,有两种不同的类型:

7(安全模式)

在V8中,同源被定义为相同。默认情况下,无法进行其他访问。如果必须这样做,则需要使用安全令牌或安全回调。安全令牌可以是任何值,但通常是唯一限定的字符串。创建时,我们可以通过指定一个安全令牌,否则V8会自动为其生成一个。

学习材料

基于V8的小程序架构

本节详细介绍了V8的小程序架构,描述了与V8的一些流程细节,以及如何与V8直接通信。

单V8结构

如上图所示,V8早期,一个小程序占用一个V8,每个V8只创建一个V8。即小程序前端框架APPX的代码appx..min.js和小程序的业务代码..js运行在同一个V8上。这样的设计会有JS安全问题。业务JS代码可以通过拼接和模拟的方式访问注入到APPX中的内部JS对象。在同一个V8中,业务JS代码和APPX框架JS代码是无法隔离的。的操作环境。稍后我们会介绍如何解决这个安全问题。

部分过程

如上图所示,分别通过.log和[9]接口进行直接双向通信。

容器到

容器需要通过加载并运行一些JS脚本;在运行一些JS脚本(af-appx.min.js和.js)之前,需要提前注入APPX框架所需的全局JS对象,例如: [10]等供调用使用。

到容器

对容器的调用本质上是通过.log[11] Web API 实现的。

部分过程

到容器

与上节类似,初始化V8时,也需要将这个全局JS对象注入到V8环境中。的定义在.js[12]中,.js[13]已经在V8中提前加载了。

AlipayJSBridge = { //xxxxx call: function (func, param, callback) { nativeFlushQueue(func, viewId, JSON.stringify(msg), extraData); } //xxxxx }

同时我们在V8环境中提前注入了该API,并绑定了该API的JAVA端回调:

mV8Runtime.registerJavaMethod(new AsyncJsapiCallback(this), "__nativeFlushQueue__");

这部分是通过.call()调用的,最终会回调到容器端的()。

容器到

容器端处理完成后,如果有返回结果,则返回。

和沟通

基于容器总线的消息通道

以发送消息为例,流程大致如下:

消息通道基于

可见,在基于容器总线的消息通道中,一条消息从开始到中间需要多次序列化和反序列化,这是一个非常耗时的操作;不仅会影响小程序在启动过程中的启动速度,而且程序滑动等交互事件之间也会有大量的消息传递,因此也会影响帧率。

于是,基于消息的通道应运而生。

允许我们创建一个新的消息通道并通过它的两个属性发送数据。如下图所示,将创建一个管道。管道的每一端代表一个,既可以向对方发送数据,也可以接收对方发送的数据。利用 的特点, 与 之间的通信不需要通过总线,减少了消息的序列化和反序列化。

V8接入JSI后台

随着支付宝及整个集团越来越多的业务使用V8引擎,V8引擎的升级和维护变得越来越复杂和重要。每个业务可能使用不同的接口,在升级V8引擎时需要重新适配。同时,前面也提到过,V8引擎目前是内核提供的,需要再次复制才能使用V8。

如何解决这些问题呢? “计算机科学中的任何问题都可以通过添加间接中间层来解决”,于是JSI()诞生了。

JSI简介

JSI()封装了引擎(V8、JSC等),为业务方提供基本、完整、稳定、向后兼容的Java API以及独立于具体JS引擎的API。

JSI带来的优势是:

基于JSI的V8

下图是基于JSI的V8项目结构。对比基于J2V8的V8[14],发现小程序、小游戏、Cube等业务只需要通过JSI的Java接口加载V8引擎即可。 JSI中使用U4加载.so,UC SDK中的.so可以复用。 ,无需复制,解决了与UC共存同一进程时.so全局变量冲突的问题。 JSI还提供了Java和C++两种封装API,方便业务接入。

JSI接入文档详细介绍了如何通过JSI快速使用JS引擎:

V8如何解决JS安全问题

前面提到,使用单V8单V8结构的V8会存在JS安全问题,无法隔离业务JS和前端框架JS的运行环境。下面介绍多隔离V8和多隔离多线程。

更多隔离

下图描绘了V8的多V8隔离架构。对于同一个小程序,在同一个V8下,分别是小程序前端框架脚本(af-appx..)、小程序业务脚本(..js)和小程序插件[15]脚本(/..js) 创建单独的 APPX、Biz,(jsi:: 对应于 v8::)。同一个小程序可能有多个小程序插件,每个插件都会分配一个单独的V8运行环境。

正如V8安全模型[16]中所描述的,同源性被定义为不同的并且默认情况下不能相互访问的,除非设置安全令牌。利用这一特性,我们对前端框架、小程序业务和小程序插件的JS运行环境进行了安全隔离。

多隔离多线程

在小程序中,一些异步处理任务可以放在后台线程中运行。执行完成后,将结果返回给小程序主线程。这就是多线程。

上图描述了多线程的设计框架。小程序的主线程运行在单独的V8上。同时,业务JS、APPX框架JS、插件JS都将运行在自己的V8上。同时,对于每个任务,都会启动一个单独的线程来创建单独的V8和V8实例。每个任务和小程序主线程中的任务都是线程隔离的,相互隔离。

隔离是指V8堆的隔离,所以主线程和后台线程不能直接传输数据。主线程和后台线程如果要实现数据传输,就需要进行序列化和反序列化(and)。序列化是指将数据从源V8堆复制到C++堆,反序列化是指将数据从C++堆复制到目标V8堆。主线程和后台线程通过序列化和反序列化接口传输数据。

JS引擎能力输出

支付宝中的其他一些业务,比如()希望获得C++层的JS引擎能力,但同时又不想去麻烦重新连接JS引擎​​。这时候V8就需要具备向外界输出小程序的JS运行环境的能力。 V8 插件就是这样的解决方案之一。

V8插件

下图描述了V8插件的框架。设计思路如下:

插件业务通过接入V8插件将获得以下能力:

由于插件业务可以直接获取小程序JS的执行环境,因此插件业务必须是可信的,否则会造成安全问题;因此,V8 java层需要对插件进行白名单管理和切换控制。

V8性能优化并行初始化

V8最初引入的原因是为了解决小程序以及串行初始化和执行的问题。之前已经介绍过,这里不再重复。

代码

上图是V8代码的原理。由于JS是JIT语言,V8运行JS时需要先解析编译,所以JS的执行效率一直是个问题。 V8代码的原理是,当第一次运行JS脚本时,会生成JS脚本的字节码缓存并保存在本地磁盘上。当你第二次运行相同的脚本时,V8可以使用第一次保存的字节码缓存重建结果,因此不需要重建。这样,第二次使用Code后,执行这个脚本就会更快。

V8代码分为两种:

Code生成的缓存会更加完整,热点功能的命中率会更高。同时,大小也会变大,因此第二次从磁盘加载缓存会花费更多时间。 V8官方宣称Code相比Lazy Code将求和时间减少20%-40%。事实上,我们通过实验发现Code并不比UC现在的Lazy Code好。原因是缓存大小对性能影响巨大。不过通过分析,使用Code时相比不使用Code时,JS执行时间还是有明显提升。

相关链接

[1]

[2]

[3]+/-/6.8//

[4]

[5]

[6]

[7]

[8]

[9]

[10]

[11]

[12]

[13]

[14]

[15]

[16]

福利来了|下载电子书《小程序开发无需求助》

本书系统全面地讲解了支付宝小程序的开发技术。语言幽默生动,带领读者从零开始充分体验小程序的开发工具、基本语法、开发框架、实现流程、快速示例和扩展场景开发。它以深入浅出的语言进行了解释,以帮助读者。快速掌握小程序开发技能。适合对 HTML、CSS 和 JS 有基本了解的读者。

分享