找回密码
 会员注册
查看: 19|回复: 0

贝壳Flutter动态化原理与实践

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-10-10 21:38:54 | 显示全部楼层 |阅读模式
贝壳Flutter动态化原理与实践 贝壳Flutter动态化原理与实践 租赁&新房团队 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2021年01月21日 20:23 一. 背景在业界很多公司面临客户端iOS、Android人力不匹配的情况,这样会阻碍业务快速落地。在贝壳,很多团队也面临着同样的问题,我们经过业界的跨平台技术的对比,发现Flutter在性能体验、稳定性以及高度的平台一致性上独树一帜,同时活跃的社区为开发者提供了很大的帮助。最终贝壳选择了Flutter跨平台技术来解决人力不均衡的问题。贝壳Flutter技术经过一年多的沉淀,已经形成了一套完整的生态体系,并且提效显著。但Flutter在贝壳落地的过程中,由于Flutter本身无法拆包,无法热修复,无法及时上线,还有包大小暴增等问题,让深度使用Flutter技术的团队非常头痛。目前业界对Flutter侧的动态化方向非常关注,但并没有一个成熟的方案来解决这个问题。所以为了赋能Flutter技术生态,动态化方案日趋紧迫。二. 技术选型我们在动态化项目初期调研了业界Flutter动态化方向的落地情况。官方曾经推出过基于产物替换思想的动态化方案,也就是解释执行方案(JIT),并且支持差量下载。不过这种方案的弊端很明显,性能无法与AOT模式相比,另外官方对安全性也有所顾虑,最终在2019年4月叫停。目前业界有两种主流方案:第一个方案是借助JavaScript来实现逻辑处理,通过Skia引擎渲染Flutter视图组件。这种方案,在运行过程中需要频繁的进行JS和Flutter侧的通信,存在性能瓶颈。另外,对端上开发同学来说,开发习惯并不友好,具有一定的学习成本。腾讯等公司采用此方案,并且技术开源。另一种方案是对源码进行词法语法分析,按照文件粒度生成AST对象,AST对象包含了Flutter页面的所有信息,利用这些信息可以便捷地生成AST Json树。在Flutter运行时,通过Runtime解释器来动态解释AST Json树,最后通过代理交给Dart VM来执行。此方案由于真正执行主体是Dart VM,所以能够保证高效的性能。美团采用此方案,但是技术闭源。由于各公司自定义方向的代码并不能统一并且有的方案技术闭源,所以贝壳成立了Flutter动态化项目组,进行动态化自研。三. 技术选型贝壳在2019年开始对Flutter动态化进行研究并技术演进,项目代号"JAYE”。JAYE初期利用JSCore的能力做逻辑处理,用Skia引擎来渲染页面。由于需要建立大量的JSBridge桥来与Flutter视图做通信,所以在性能上稍有不足;并且JAYE初期的版本业务同学需要使用JS语言来开发页面,那么存量的Flutter页面将无法动态化。基于技术栈更内聚,且性能更强大的初衷,我们对JAYE做了技术演进。大体的思路是在不打破Flutter技术语言和执行环境的基础上,使用DartVM来执行逻辑,用Skia来渲染视图。演进版本由租赁团队和新房团队联合开发,项目代号“JAYE 2.0”,目前已在贝壳系的小流量业务App落地验证,并且效果极佳。JAYE 2.0,为了兼顾开发成本以及高性能,决定不改变开发同学的开发习惯,使用Dart源码转换为AST JSON树,再通过内置Runtime 指令代理的方式,形成了一套在纯Flutter执行环境下,高性能执行的动态化技术框架。整体架构:四. 动态化原理4.1 编译期在编译期我们对Flutter页面进行AST树分析,最终形成页面的原子描述(AST JSON)4.1.1 什么是AST树AST意为抽象语法树,是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构。编译器的编译流程是很复杂的,但是经过分析,最终我们只需要关注词法分析和语法分析,这两步是从代码生成AST的关键所在。4.1.2 函数转化AST上面的代码首先拿到的是一个语法块,是一个FunctionDeclaraction对象,这个对象包含了两部分:1. id 方法名;2. expression,方法描述。对于id,很好理解,就是方法名,不需要继续拆下去。expression是对方法的描述,又包含了参数列表parameters和body两个对象。parameters是一个数组结构,定义了参数的名称以及类型。name为参数名称,paramType定义了参数的具体类型。body是一个block结构,定义了方法的主体及返回值类型。BinaryExpression 也就是二元运算,主要包括三部分:1.left,左对象;2.right,右对象;3.operator,操作符。这三部分就可以定义一个基本的加减乘除计算。接下来看下对应的解释器,是如何执行的。根据操作符,以及左右对象,通过switch case即可执行所有的运算逻辑。这就是基本逻辑的AST转换以及运行时的逻辑执行。4.1.3 组件转化AST这种方法可以将源码所有想表达的信息,都完整的描述出来。对于AST JSON的结构,后期我们会根据需要,进行特殊节点的标识与定制。4.1.4 对于imports依赖针对当前Flutter页面依赖的类以及数据,我们会根据Imports递归加载,并且对Yaml文件分析,最终生成主Flutter页面所有imports依赖的JSON文件。依赖的JSON文件中会包含Flutter页面所有依赖的信息,并且和主Flutter页面的AST JSON进行关联,方便后面解释器查找依赖信息。实际场景中,会存在逻辑和widget嵌套,以及多层逻辑嵌套的场景,原理上都是一样的, 最终都会被拆分成很细的AST节点对象,由解释器来解释并发送给Dart VM来执行。4.2 运行树4.2.1 项目架构Flutter动态化运行期原理可以简单理解为解析AST JSON并将其映射为相应的Flutter AOT代码并执行。解析过程将JSON文件解析为相应的数据结构,包括library、class、function、variable、指令等等,其中指令分为两种,一是基础语法指令,比如if else 、赋值语句等等,二是Framework System API。映射执行过程本质上是一层代理的模式,其中Framework system API类型的指令挂一层映射,直接代理执行,而基础语法指令则是需要构造一个运行期的VM的上下文环境去辅助执行.运行时结构图:4.2.2 类描述与结构定义通过获得已经结构化的AST语法树JSON,下一步我们需要转换成机器可识别执行的指令,作为动态化加载,我们必须构造自己的方法区代码,将已有的JSON信息解析为类信息存储下来,构建一个元类系统。当该类第一次被使用的时候,通过已有的类信息初始化为某个实例。首先是文件的解析,以懒加载的形式解析出Library对象,用来存储顶级变量、顶级方法和类的指针,然后是类的加载需要存放的信息,有类名className,实例变量variableMap,静态变量staticVariableMap,普通functionMap,构造方法constructorFunctionMap,父类关系superClass,文件关系对应的文件索引等。 寻址的过程为本类->父类->文件,针对父类的属性和方法,为了避免本类臃肿,我们通过指针的方式将父类的属性和方法记录在本类中。同时为了保证系统类的生命周期,使用mixin的方式,将类属性注入到代理中,下图为类结构。4.2.3 方法执行栈、作用域和寻址由于我们解析的类继承自系统Widget类型,所以需要Hook掉系统的生命周期方法,从而可以实现动态化加载我们自己代码的过程,invoke方法作为方法执行的总入口,方法执行需要参数参与计算,在参数寻址的过程中需要构造作用域,作用域通过嵌套的形式,传入方法体中,递归时可以通过反向递归来查询参数值,函数内部可以取到外部作用域参数,而函数外部无法取到内部作用域参数,公共参数可以通过父节点向上取值。4.2.4 指令映射和指令代理指令映射分为三种类型,Widget方法映射,基础语法映射,自定义方法映射。指令代理结构,如图:简单语法的指令执行,JSON语句通过之前的解析会映射成对应的语句对象,比如我们会将switch语句解析成一个SwitchStatement对象,当判断类型为switch语句类型,会将指令分发给固定的方法,再按照switch的语法逻辑处理,判断checkValue的取值和SwtichCase对象里condition的取值的关系,执行对象中statements数组里面的语句,然后跳出switch语句,这就是一条switch语句的执行过程。并且语句的执行可能是一个嵌套,递归的过程。示例:switch语句对像。示例:switch语句指令分发执行4.2.5 Widget视图渲染视图类Widget解析,在Flutter 框架中我们有大量的布局类Widget和渲染类Widget需要代理,此外我们还有各种公司的组件库需要代理。我们为每一个需要代理的Widget定义固定的key,通过Key->Value的形式将原生的JSON中Widget的字节码映射成WidgetBuilder的代理构造器,利用代理的Build方法和JSON中的参数,构造生成Widget对象插入到系统的Widget树中,完成渲染工作。为了方便拓展新的Widget,为这个结构设计了一个简单的工厂模式。4.3 单元测试在动态化项目组开发调试的过程中,我们每次调试一个组件或者逻辑都需要从头开始跑一个完整的链路。因为我们的链路较长,如果一个环节出现异常,就会导致调试失败,这样大大浪费项目组的开发调试时间。所以我们设计了一套更原子的测试框架,通过对独立执行单元进行测试,从而缩短我们的测试链路。这样大大的缩减了项目组的调试人力成本。五. 落地实践经过我们在业务中落地之后的性能指标分析与收集,整体性能指标媲美纯Flutter页面。内存方面,Flutter动态化页面的内存占比和原Flutter页面相差无几。在帧率方面,Flutter动态化页面帧率与原Flutter页面保持一直。一期我们已经在掘金App落地了动态化方案,并且借助hotFix的能力修复了多个线上bug,整体修复过程非常高效。由于动态化提供的热修能力,能够给我们的App提供一套更健全的质量保障体系,因此后面会推广到更多的App落地。六. 总结&规划JAYE 2.0通过编译期生成描述,运行时动态解析并交由Dart VM执行的设计思路,实现了未打破Flutter技术体系以及运行环境的动态化方案 。JAYE 2.0既可以实现完整页面粒度的动态化效果,也可以实现细粒度的视图模块、方法模块动态化效果。同时经过测试,JAYE 2.0在性能上可以完全媲美纯Flutter页面。JAYE 2.0的线上效果非常不错,但从一个更健全的动态化体系出发,我们仍有很多事情要做。目前我们正在以下几个方向着重发力。6.1 系统快速扩充自动化工具JAYE 2.0在动态化的过程中需要挂载大量的代理来交由DartVM来处理。其中包括系统视图,系统逻辑,自定义视图,自定义逻辑,以及插件等等。这些代理的工作量会非常大,并且很多都是重复的。这样会耗费大量人力,并且系统不能快速扩充。所以我们决定开发一套自动化工具,来解决这个问题。目前自动化工具已经验证方案可行,正在完善中。6.2 业务高效开发IDE语法检测目前JAYE 2.0已经支持的大部分语法,但有些自定义的组件以及逻辑还需要不断的补充。所以业务接入动态化开发页面的过程中,我们需要把动态化不支持的语法提前暴露出来。以免开发同学在完成页面之后才发现问题,这样能够避免人力浪费。源码地址映射在开发过程中由于我们最终运行的是动态化原子执行单元,那么出现问题后我们如何能够快速定位到问题呢?我们设计了一套源码地址映射的方案,在编译期我们会对生成的ASTJson和源码进行映射。有了这个映射之后,我们就能够快速定位到源码位置。6.3 前端技术栈打通上文提到了动态化在运行时需要的就是ASTJson文件。那么我们是不是可以把前端同学编写的H5页面也通过词法语法分析,生成动态化需要的ASTJson呢?答案是肯定的。但是由于H5页面的编写是面向过程的,而Flutter页面的编写是面向对象的,所以在树形结构上会有很大的差异。后续我们会做一套针对树形结构的语法转换器,来映射成JAYE 2.0需要的ASTJson文件。七. 最后JAYE 2.0可以打破常规的双周迭代模式,并且可以使各个业务线之间相互独立互不影响,同时也增强了Flutter线上的修复能力,由于业务代码和资源可以远程下发,所以还可以减少Flutter的包体积。JAYE 2.0 在贝壳产品体系的落地,促进了贝壳在Flutter前言技术领域探索的发展。在未来我们还会持续建设和推进JAYE 2.0的体系建设,提升开发体验,覆盖更多场景。贝壳租赁团队长期招聘iOS、Android、H5、JAVA、PHP P5/P6/P7工程师,欢迎加入贝壳大家庭。感兴趣的同学发送简历至韩旭的邮箱:hanxu005@ke.com。 预览时标签不可点 Flutter15大前端69移动端37Flutter · 目录#Flutter上一篇Flutter图片内存优化实践下一篇Flutter的Widget之间的通信方式及状态管理关闭更多小程序广告搜索「undefined」网络结果
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-28 06:22 , Processed in 0.805662 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表