UC Flutter技术沙龙分享:Aion - 拥抱 Flutter 生态的动态化方案

6,815 阅读28分钟

招贤纳士​

我们急切需要浏览器渲染引擎/Flutter 渲染引擎的人才,欢迎大牛们加入我们

前言

1 月 16 日,UC 技术委员会联合掘金、谷歌开发者社区举办了 2021 年首届 Flutter 引擎为主题的技术沙龙活动。活动吸引了 150 多名同学报名,由于疫情控制现场人数的影响,最终我们只能安排 50 名同学来到现场。另外,有 2000 余名同学围观了现场直播。活动中,来自阿里巴巴集团内的五位技术专家与大家分享了基于 Flutter 构建的研发体系,开发与优化经验,动态化方案,以及 UC 定制的 Flutter 增强引擎 Hummer 的优势与新特性。

第一场分享是由 UC/夸克客户端负责人、UC 移动技术中台负责人辉鸿带来的《打造基于 Flutter 的 UC 移动技术中台》。

第二场分享是由 UC Flutter Hummer 引擎技术负责人佬龙带来的《Hummer (Flutter 定制引擎)优化及体系化建设探索》。

第三场分享是由 UC 浏览器视频技术专家礼渊带来的《基于 Flutter 的移动中间件技术体系和音视频技术》。

第四场分享是由闲鱼移动组技术专家云从带来的《闲鱼在 Flutter 上的体验优化实践》。

第五场分享是由 UC 浏览器跨平台方向技术专家步八带来的《Aion:拥抱 Flutter 生态的终极动态化方案》。本文约 9000 字​,30 张图片。​

分享内容

大家下午好,我是来自 UC 浏览器研发部的步八,今天跟大家探讨的主题是基于 Flutter 原生态的动态化方案。 

 整个分享会分为 4 个部分,第一部分会给大家介绍一下 Flutter 在动态化技术这一块的演进情况。第二部分会介绍一下 UC 在 Flutter 动态化这块的整体技术方案。第三部分会展开讲一下我们的方案在落地过程中遇到的一些技术挑战。最后一部分会分享一下当前的一些进展以及未来的规划。 

 移动端的容器化技术发展过程从最近几年来看,一直处于持续火热的状态。在 Web、端原生这种传统容器方案之外,涌现出了像 Hybrid、Weex、RN、DynamicX 等不同解决方案,整体发展呈现百花齐放的状态。在整个发展的背后动力,更多来源于就是业务方对于动态性、高性能、跨平台、高效能这几块核心能力的强烈诉求,业务方的要求变高了,既需要高动态性,也需要高性能、强交互的能力。 

我们看到现有的容器技术的演进思路,通常是基于传统的容器方案的技术链路的拆解和重新组合,进行优势的互补,弥补原有的瓶颈,就像 Web 跟原生端的结合产生了 Weex 的方案,还有最近比较火热的 Web 跟 Flutter 的结合。从当前的现状来看,依然没有大一统的解决方案,依旧处于一个持续优化的过程, Flutter 的出现也并没有很好的解决这个问题。 

在 UC 我们首次在首页一级页面采用了 Native 原生之外的方案,以及创新 APP 的场景,在之前是没有过的。Flutter 的核心优势在于它在跨平台,高性能强交互这块极致的体验,当然在应对一些复杂的场景依然存在非常大的压力,这块相关的优化可以参考佬龙的分享。Flutter 在动态性这块存在比较大的缺失,所以在 Flutter 诞生开始,就有非常多的技术团队,展开对 Flutter 动态化的一系列探索研究。 

接下来简单介绍一下行业在 Flutter 这块的一些动态化技术方案。第一套方案是 Web 跟 Flutter 的一个结合,就是前面说的利用 Web 生态 js 的高动态性跟 Flutter 本身的渲染流程去做结合,达到一个既能够满足高动态性,也能满足跨平台一致性以及高性能交互的体验。从结合的点来看,根据节点的选择不同又会细分为几套方案,这里不做展开。 

然后说下我们对这个方向的思考,Flutter 的核心优势,来源于 Widget 设计、渲染流水线设计还有 Dart 极致的 AOT 优化,其中每一块都是作为生态的部分去优化演进的,其背后强大的开源生态为其不断技术升级提供了强力的支撑。所以我们的思路还是希望能够基于整个 Flutter 的原生态去解决动态化的问题,这样我们才能够最大化的利用整个 Flutter 生态的优势。 

第二套方案我把它简单概述成 Simple VM 的方案。整体的方案思路如图,第一块是 Dart 的原始代码,实现了 Widget 视图界面,经过前端的编译生成 AST 产物,然后在编译阶段将 AST 产物转化成 JSON 的数据描述,JSON 文件下发到客户端,在 Dart 层对它做一次解析,把 Widget 描述映射成对应的真实的 Widget,这样就构建了一颗完整的 Widget 树进行渲染,这个是视图层的处理,逻辑这一块会把 AST 里面逻辑去分离出来,然后通过在 dart 层实现简单解释器进行处理。 

AST 其实属于一个相对高级的编译产物,基于 AST 的解释执行在语法层面会有比较大的限制,很难做到跟 Flutter 的原生态做一个完美的兼容。所以我们评估这套方案在支持非常复杂的业务场景时,会遇到比较大的瓶颈。当然除了这两个方案之外,其实还有其他的一些像双引擎的方案,这里不做展开。 

对第一部分做下总结,Flutter 凭借高性能跨平台的特性以及背后的强大开源生态支持,近几年增长非常迅猛,但是动态化能力依然是业务方的一些核心的诉求,基于 Flutter 生态的动态化方案,依旧处于一个缺失的状态。 

接下来的部分给大家介绍一下我们在 Flutter 动态化这块的思考和整体解决方案。 

首先说一下目标,前面提到我们希望基于 Flutter 的原生态的方向去走,所以我们希望整体方案跟 Flutter 生态的研发链路是完全一致的。我们希望能够做到对业务方的整个开发过程不产生任何干预和约束,这是第一点。第二点也是一个比较重要的点,我们希望在加入了动态化的能力之后,整个核心的一些性能体验其实无限于接近于现有的 AOT 模式。第三个点是我们希望引入到这个方案之后,不会对整个研发的效能产生一些负担。这是我们最开始去思考整个方向的时候,设定的一个比较理想化的目标。 

先简单介绍一下整个 Flutter 的编译流程。前面佬龙也简单介绍过,整个 Dart 的编译流程其实分为前端的编译和后端的编译,前端的编译主要会生成与平台无关的一些中间指令,后端的编译会把这些中间的指令最终编译成平台的机器码。然后在前后端的编译流程中,都有一个非常重要的环节,就是编译优化。 

首先看 Dart 前端的编译,Dart 源代码会通过前端的 CFE 流程生成 Kernel 文件,Dart 提供了比较完善的工具链,能够将 Kernel 文件转成抽象语法树(AST),生成 Kernel 文件后,前端编译器会做一个编译的优化,通过 TFA 的推导,核心包含把一些无法触发的代码去除以及函数调用的去虚化处理;然后到了后端,也会经历编译的优化,这块优化会更激进一点。这个是整个前后端编译的流程,其实中间还有一块的流程,就是我们能够通过 Kernel 文件去转成 Kernel ByteCode 文件,这个也是 Dart 支持的。 

我们的整体技术方案其实是希望把业务的动态化代码能够去剥离出来,然后通过解释器的方式去执行,其它还是基于 AOT 模式运行,通过这种混合执行模式来达到动态化的目的。这里面的技术选型涉及到产物的选取问题。从整个编译链路来看,其实有三种选择,第一种是 AST 产物,这块在上面的介绍中其实已经被否定掉了。剩下还有两条路线可以选择,第一种是基于 Kernel 去解析,另外一种就是基于后端优化过之后的 IL 指令去解析。

基于 Kernel ByteCode 去解析是有完整的解释器支持的。但是它的缺点其实也很明显,它的整体性能因为没有经过后端更激进的编译优化,所以它的整个指令的执行性能会偏低一点。IL 这一块的缺点在于我们需要去进行完整的 IL 解释器开发。我们目前选择了第一种方案,这里面的一个核心技术判断是我们认为 CPU 密集型的计算,像排版渲染这块的执行其实都是在 AOT 中,业务代码其实对 CPU 的占用非常低,这样整个综合下来的性能,折损不会太严重,所以我们最终的方案其实是基于 Kernel ByteCode 作为动态化产物,结合解释器的模式去达到动态化的效果。 

先讲一下整体流程架构,主要分为两部分,第一部分是在开发阶段,通过对新旧分支去做前端的编译,通过编译之后我们能够得到新旧的 Kernel 文件,然后我们会通过一套 diff 的依赖算法去生成 diff 文件,然后再把它转成 Kernel ByteCode的产物,最后下发到客户端这边,端侧通过 Dart 虚拟机层面支持的字节码解释器去执行,同时我们需要去解决 AOT 跟 KBC 的函数相互调用的问题。资源这一块也是通过一套 diff 的方案去生成,打包到 Patch 产物中,我们会去修改 Flutter 资源访问的流程来达到资源更新的目的。这个是整体的方案介绍,接下来会详细展开。 

整个方案的核心需要解决三个问题:  

第一个是 AOT 的编译,编译产出基础的 APP 包,我们需要提供加载 Patch 的入口,包括 KBC代码和更新的图片等资源; 

第二个是 Patch 包的编译,这一块目的主要是需要分离出最小化的动态的 KBC 代码。这里加了一个最小化。本质上我们可以把所有的业务代码编译成 KBC,然后通过 main 入口插桩走解释器的模式去执行。但是这里面存在的问题在于可能我们的功能开发只修改了少部分的代码,这种场景下希望能够把这个变化点控制在一个很小的范围内去走解释器的模式去执行。其它的内容还是走 AOT 的模式,这样能够极大的提升实际运行时的性能; 

第三个是运行时这一块的支持。我们知道 Dart 虚拟机在预编译模式(AOT)模式下其实不支持这种解释器的,所以我们需要在 AOT 模式下增加对 KBC 解释器的支持,并且能够支持 AOT 跟 KBC 之间的函数互调。 

AOT 编译这一块的整个流程分为几块,第一块是我们会对 Flutter 工程做插桩的标记,就是添加一个注解。然后经过前端的编译之后会生成 Kernel 文件,通过编译 AST 寻找到插桩的的函数入口,然后插入固定的调用切换代码。插桩的目的其实是为了能够提供一条通道能够去加载 KBC 的代码,最后通过 TFA 优化流程之后生成 AOT 产物。 

分离动态化内容包含 KBC 代码及资源文件,代码这块采用的是对基线跟Patch的分支去做二次的编译生成新旧的 Kernel 文件,然后依赖一套 diff 算法去生成 Kernel diff ,然后再通过 Dart 本身提供的工具编译成 KBC 的产物。diff 算法这块后面会展开来讲;资源这块其实我们是通过 git diff 的工具把变更的资源提取出来,跟 KBC 这块一起打包成最终的 Patch 包资源。 

运行时依赖模型怎么去理解?在运行时需要去解决两个问题,第一个问题是符号隔离的问题,第二个是依赖解耦的问题。举例来看,左边是在 AOT 模式下面写的一个简单 case,ClassA 跟 ClassB 之间是相互调用的关系,如果我们在Patch上修改了 ClassB 及新增 ClassC。 

第一个问题,因为 AOT 中的 ClassB 是依然存在的,那么我们需要去解决在 AOT 中的符号跟 KBC 中的符号冲突的问题。我们会在 KBC 里面的所有节点的 Uri 去做统一的修改,这样能够避免 KBC 的符号跟 AOT 里面的符号冲突。 

第二个问题是 AOT 代码是没办法去直接调用 KBC 的,所以我们需要通过约定的通道,去实现 KBC 的加载。前面已经介绍过,插桩的方案能够解决从 AOT 到 KBC 的访问,但是插桩的方案只能解决函数依赖的问题,但是现实中的依赖关系可能会非常复杂,例如 A 跟 B 之间的依赖关系,可能涉及到继承、泛型引用等复杂依赖,这个是需要我们去解决的。

接下来介绍下混合运行机制,混合运行的模式下面,运行时内存模型分为三类,一类是虚拟机本身的代码,由 C++ 编译器决定;一类是 Dart 代码( AOT 模式 ),由 Dart 编译器决定;另外一类是新增的字节码,由解释器决定。上图是解释器的主要内存模型,包含函数栈及栈帧、对象池、模拟的寄存器等,堆这块的分配和 AOT 模式保持一致。 

这里主要是介绍下 AOT 和 KBC 函数相互调用的问题。函数调用我们分为两个部分,先说 KBC 对 AOT 的函数调用,介绍两种函数调用字节码指令: 

  1. DirectCall 指令,能够在编译期直接确定的函数调用,解释器会通过 ObjectPool 获取函数地址,ObjectPool 属于函数的字节码常量内容,在加载函数时会进行解析,包括函数引用对应的函数地址; 
  2. InterfaceCall 指令,在编译期没办法确定要调用的函数是哪一个,需要在运行时根据目标对象类型去做查找。动态查找时先在缓存进行查找,查找不到进入全局 library 数据结构(加载 KBC 时会合并字节码 library数据)根据函数名和 receiver 进行一层一层查找,找到后放入缓存并返回,返回后的函数根据是 AOT 还是 KBC 函数走对应的执行流程; 

第二块 AOT 对 KBC 函数是无法直接调用,一般是通过虚函数隐式调用,这块的函数调用的问题涉及到 AOT 的函数调用机制。在 Dart 里面有两套函数调用机制,一种是 dispatchTable的机制,另外一种是 SwitchableCall的机制,这块的内容在后面的部分去展开去讲。

总结一下整个方案,主要做了两件事情,第一件事情是我们会对整个编译器的流程去做改造,目标是分离出最小化的字节码动态产物。第二件事情是我们需要去对整个 Dart VM 去做增强支持 AOT 和 KBC 这种混合运行模式。 

接下来一部分主要讲一下我们在落地整个方案过程中遇到的一些大的技术挑战,在介绍整体方案之前,给大家看一下我们整个工程接入这块的情况,让大家有一个感性的认识。

在前面其实已经说过,我们希望这套方案对开发者来说是非常友好的,不需要去做太多的事情,就能够去使用这套动态化的方案。整个接入过程分为三个部分。 

第一步需要去标记整个业务入口函数,加一个注解就可以了,怎么去加这块后面会讲。 

第二步需要接入方实现一个环境变量,这个是为了设置动态化的资源的路径。 

第三步我们会提供自建的打包命令,输入基线的分支以及patch的分支,就能完成 Patch 打包,打包输出这里面包含 KBC 的代码、资源以及相关的一些配置文件。 

这样整个接入流程就完成了,当然这里面有一个前提,就是引擎这块需要接入 Hummer 引擎。        

介绍下第一块挑战,刚刚前面说过,分离 KBC 动态代码需要解决运行时依赖的问题,上图是 Dart 工程的 AST 结构图,每个 Dart 文件对应着 Library,Library下面会有静态变量、函数及类,类下面又会包含变量以及函数,我们会对新旧分支的 AST 产物做一次 diff 对比,对比过程比较简单,每个节点都会有一个 reference 信息,我们会根据 reference 信息建立连接,判断它是不是相同的节点,如果不是,标记为新增节点,如果是的话,我们会继续去做一次深度遍历对比,去判断它的孩子节点是否发生变化,以及变化类型,比如函数的变化,可能包含返回类型的变化,函数体的变化,泛型的变化等。然后基于这一套操作之后,我们就能够得到了新增以及变更节点信息。 

接下来进行第二步的操作,左边这个方框之内的就是前面提到的得到的原始变更数据,我们会去做一个分类,如果只是函数体 Body 的变更会把它放在下面(黄色部分),其它的节点会放在上面(橙 色部分)。 

做完这一步之后,接下来我们还要去做依赖的搜索,基于上面这一部分(橙色部分)去查看它的依赖关系,然后把它所有的依赖搜索添加进来。搜索添加进来的节点,也会按照第一步分类的原则去分类。如果是函数体 Body 的变化或者是函数体内部的依赖,都会放在下面,其他的会放在上面。当然这个是一个循环的操作,直到没有新增的节点产生。然后经过这一步操作之后,我们会把上面橙色部分的内容都标记为 KBC 内容。 

下面黄色这一块的内容,它本身也是会有一些变更或者对上面 KBC 的直接依赖,但这些变更都是属于函数体的变化,依赖都是在函数体内对上面 KBC 的依赖。这些依赖或者变更,都可以通过插桩调用的方式去做依赖的解耦,插桩的对应函数内容我们会转换成闭包的函数放到一个FunctionMap里面,在运行时通过一条标准的通道去加载,这样就完成了动态加载更新的 KBC 代码的目的。 

这个方案缺点其实是比较明显的,需要去对所有的函数进行插桩的处理,因为我们没办法去判断哪些内容会发生变更或者产生依赖,这块本身是存在一些性能的影响。优点是对整个工程架构完全无限制,对业务方接入来说是完全没有成本的。第二个优点是它生成的 Patch 产物基本上是一个最小范围内产物。 

下面介绍下第二个方案,我们以 package 的维度去做更新。我们会把整个 Flutter 工程架构去做一个分层,从上往下,最上面一层是业务的 package,第二层是依赖的业务基础库,再往下是 Flutter 基础库以及 Dart 的基础库,我们会对整个工程架构做一些小小的约束:  

  1. 最上层的业务 Package 之间,是不会相互调用或者依赖,并且能够通过唯一的通道入口去加载业务的 Package; 
  2. 对中间业务依赖的基础库这一层,基本没有约束,可以相互之间调用或者依赖,但是不能向上调用到业务的 Package;
  3. 最下面的 Flutter 基础库和 Dart 基础库业务侧不会进行修改,不做任何约束;

整个约束从架构设计的角度来看,其实限制并不大,我们在对现有的工程实际改造过程中会发现这块改造成本其实非常低,因为它本身就是按照这套规范去设计的,下面看下运行时过程,当业务发生修改的时候,会有两种情况:

  1. 一种情况是我只改了整个业务的 Package,例如只改了小说这个模块,那么实际上在我们生成 KBC 的时候,只会生成小说的 KBC 代码。在运行时根据配置通过入口插桩加载对应代码,其他的 Package 依然还是走 AOT 的模式。
  2. 另外一种情况是既修改了业务的代码,也修改了基础库的代码,这种情况可能会稍微复杂一点。除了直接修改的 Package,还需要搜索业务基础库相关的依赖 Package,这些都需要打到 KBC 里面,因为业务基础库的访问入口无法限制,可能从各个不同的入口点去访问,所以如果涉及到业务基础库的修改,都需要将主 Package (main入口)打包到 KBC 里。 

这个方案是我们现在采用的方案,虽然会对工程架构进行一些约束调整,但约束限制并不大,另外业务入口的插桩标记也并不复杂,只需要增加一个注解即可。优点其实刚才已经说过了,不需要去对所有的函数去做插桩,稳定性相对来说是非常可控的。 

Patch 动态资源加载流程相对来说是比较简单的,首先我们会在编译期通过 git diff 的方式把所有变更的资源提取出来,这里面包括新增的资源以及发生修改的资源,然后根据这些信息构建对应的 manifest 配置文件Flutter 的资源都是放在 Asset 路径下,Flutter 也提供了对应的 Loader 去加载资源文件,我们会拦截这个资源加载入口,在加载的资源的时候,会根据 manifest 进行判断有没有命中,如果有命中,就会走 Patch 资源路径加载,如果没有,就会按照原始的流程去走,这样我们就完成了整个动态资源的更新。 

第二个挑战是影响最大的,就是 AOT 优化。Flutter可以拥有极致的性能,是因为它在整个编译阶段做了大量的优化,包括内联优化,摇树优化,常量优化、签名优化等,这些优化在带来性能 & Size 这块价值的同时给整个动态调用带来了非常大的问题。例如摇树优化, AOT 阶段会把没有用到过的某个方法或者 class 优化掉来降低 Size,那么在 Patch 里面对这些优化掉符号的调用就会产生问题。这对业务开发者来说通常是不能接受的。 

介绍一下我们在这一块的解决方案。核心思路其实比较简单,在 AOT 里面被优化掉了,我们可以把它编译到 KBC 里面去。但是这个方案存在几个问题需要解决。 

  1. 第一个问题,我们怎么去判断哪些优化的内容需要编译到 KBC 里面去,因为并不是所有优化掉的内容都需要,这样它的开销其实很大的; 
  2. 第二个问题,怎么去处理调用链路的变化,被优化掉之后,它的函数调用的路径是怎么样的,这个也是我们需要去解决的。

第一个问题是通过二次编译对比的方式来解决,我们会去对新旧的分支去做二次编译,然后生成 Kernel diff,我们会去判断有哪些是新增,新增的部分我们就认为是在 KBC 里面需要用到的。当然这里面还涉及到一些细节问题,例如怎么去判断这些变化,是来自于本身业务的修改,还是编译优化,这里具体的技术细节不做展开。

针对整个 class 被优化掉的场景,相对来说是比较简单的,首先我们需要去找到被优化掉的 class,然后我们把它编译到 KBC 产物里面就可以。针对函数被优化掉(这里特指的是在 class 里面的函数被优化掉),这种场景相对来说比较复杂,因为函数被优化掉,说明剩余的部分其实是在 AOT 里面存着依赖的,没办法去把被优化掉函数对应的 class 替换掉,这个是一个非常大的问题。 

下面介绍一下解决思路,如图,左边橙色是 KBC 的类,它会调用到 AOT 里的函数 Function2。而 Function2 在 AOT 优化阶段被干掉了。

方案一是去除掉这条优化策略,即保留所有被优化掉的函数。但是保留这个函数带来的问题是函数是包含返回类型,返回类型,泛型,还有函数体和传参的,包含这些内容之后可能形成一条很长的依赖链路,我们需要把后面所有的这些依赖都需要去加入进来。而这些依赖的内容很有可能在原本的 AOT 阶段其实是应该被优化掉的,这个对 Size 这块可能效果不佳。 

方案二我们切换了一下思路,我们会去设计一个桩的概念。KBC 的调用能通过 AOT 的桩,然后回调到 KBC 里面去。所有相关的依赖在 KBC 里面是存在的,这样我们就能够解决相关的依赖问题。这里面的一个核心技术点是我们怎么设计桩,在解决 KBC 调用链路的问题又不引入新的依赖,核心解决思路是我们会把所有的这些桩函数进行符号擦除,所谓符号擦除,其实就是把所有符号类型变成 dynamic 的类型,这样就不会产生新的依赖,同时它也能够去支持在 KBC 里面进行调用。 

原先的 AOT 环境下类型数量和内容都是确定的,这样编译阶段能基于这个原则进行函数调用的推导,很多虚函数的调用都能被推导为 directCall 而直接进行内联,在开放动态性后,会打破 AOT 类型的封闭性,因为有可能会引入新的类型,很多函数调用的推导就会失效,不然运行时会出现调用 KBC 函数错误的问题,这块没有好的解决方案,只能将一些推导优化策略去掉,但是我们也会增加一些条件,例如对于一些基础库保持封闭条件的推导优化。 

另外一种情况是 InterfaceCall 场景, Dart 是通过 displayTable 这套机制来解决 InterfaceCall 的问题,先简单说下 dispatchTableCall 这套机制,它是一个性能非常高的机制。它的数据模型是一个一维的线性数组,可以支持在函数调用时通过 ClassId + 偏移值(在编译期确定)确定函数地址,这套机制的实现有两个前提条件:  

  1. 第一个条件是 Dart 类的加载是基于一个深度遍历算法,所以基类的所有子类的 ClassId 是一个连续分布; 
  2. 第二个条件是 AOT 的类型环境是封闭的,数量和种类都不会发生变化; 

基于上面的条件能够在编译期通过控制目标函数偏移值将所有 Class 的函数地址分布到这样一个一维线性数组,然后运行时能够通过 ClassId 加上函数偏移值查询到对应函数的地址; 

因为 Table 并不是以 Class 的维度去去设计的,那么它带来的问题是动态新增的 Class 以及重载的函数是没有办法去添加到这个 Table 的,这样在 AOT 里面对 KBC 对象的 InterfaceCall 就会出现错误。 

解决思路是对 AOT 对象依然保持封闭,但对 KBC 对象保持开放,具体过程是在进行 InterfaceCall 时判断目标对象类型,如果是 AOT 对象依然走 dispatchTable 的查找机制,如果不是(即为 KBC 对象)则会走另外一条 SwitchableCall 的机制,这样我们能保证大部分的调用依然是一个高效调用。SwitchableCall 是一套状态缓存机制,缓存了 ClassId 到 函数地址的映射,会根据运行时类型匹配的情况会进行不同状态版本的切换,具体细节不在这里介绍。 

总结一下,除了上述的挑战,我们还遇到非常多的问题,特别是在虚拟机对接解释器这块,发现里面有非常多的 Bug,我们花了比较多的精力去解决这些混合运行下出现的问题。整个技术落地过程中最大的挑战是在尽量保证整个 AOT 极致优化成果的同时能够支持动态化的调用,保证开发者体验。 

最后一部分给大家分享一下我们现在的一些进展以及对未来的一些规划。 

整套方案目前内部已经经过了 6 轮的灰度,可行性以及稳定性经过了灰度验证。目前最核心关注的是在性能优化这块的一些处理。上面我列了核心的性能相关指标,从数据可以看到,AOT 模式下面其实也是会存在一定的性能损耗。从技术原理的角度来看,这块的折损是不可避免的。在 KBC 动态运行模式下的整体性能也存在一些差距。后面会给大家看一下真实的体感,相对来说体感差距不是很大。

看一下实际的效果,这是前面提到的幽默业务场景,幽默业务是一个典型的信息流产品,具备多 Tab 、长列表、丰富的卡片内容等特征,整体的交互其实非常复杂,大家可以体验一下,左边是我们现在线上的 AOT 的版本,右边是动态加载了幽默 KBC 业务代码后的体验,包括整个首屏加载及交互。从体感来看,差距并不是特别的大。 

最后同步下近期的一些规划,从短期来看,我们还是希望围绕着当前的 AOT + KBC 混合执行的方案去把整体性能还有稳定性优化到更好的状态。因为从前面数据来看,当前整体性能与线上 AOT 版本还是会有点差距的,我们正在去探索这块的优化方案。然后从长线的方案来看,在前面也已经探讨过,基于 IL 指令的解释器可能是更先进的解决方案,因为在后端这一块会做更多激进的策略优化,基于这个方案我们能够把整体性能提高到一个更高的水平。

关注公众号请搜索 U4内核技术,即时获取最新的技术动态