Fair下发产物-布局DSL生成原理

柯超2022年11月15日大约 9 分钟

大家都知道,Flutter在release环境是以AOT模式运行的,这就决定了我们要做动态化的话无法简单的通过动态下发dart代码执行的。根据Fair团队的前期调研,我们对布局动态化和逻辑动态化的实现采用了两套不同的实现方案,对于布局部分,我们在解析dart源文件之后生成DSL产物下发,然后在端上解析DSL构建布局的方式,逻辑动态化的部分,我们采用的是dart源码转js下发的方式,具体原因,可参考前文《Fair 逻辑动态化架构设计与实现》,此处不再详述。
整个动态化产物大致如下:
图片本文主要介绍的正是DSL生成的整体流程。
关于JS部分产物生成逻辑,请参考接下来的连载《Fair下发产物--逻辑JS生成原理》。
文章的整体章节包括:

整体流程概述

  • AST解析
  • DSL生成
  • 总结

整体流程概述

详述具体流程之前,我们先来看看整体的流程,然后再去讲解各个流程的原理细节。

整个流程大致分为两部分:

  • 通过fair_ast_gen将源码解析并生成AstMap
  • 通过fair_dsl_gen将AstMap转换成我们需要的Fair DSL

这里涉及到两个概念,大家需要先了解一下:

  • AST 全称是Abstract Syntax Tree,中文名为抽象语法树
  • DSL 全称是Domain Specific Language,中文名为领域特定语言

这两个实际上都是与编程语言无关的概念,对这两个概念的理解比较重要,不过受限于篇幅,本文无法详细展开来解释,之前未接触过的建议先搜索做个大致的了解。

AST解析

源码解析

要把dart源码转换成我们需要的DSL,首先要对dart源码进行抽象语法分析,这里是整个转换过程的第一个关键点,甚至可以说是整个DSL生成的基础。好在dart 官方提供了解析工具包analyzer,这为我们整个的dsl生成工作大大减轻了工作量。

analyzer包的utilities类提供了parseFile函数,这个函数返回的CompilationUnit,实际上是一个编译单元,继承自AstNode,正是我们后面AST解析的入口。

AST解析

前面我们提到,AST是一种与编程语言无关的抽象语法树,对于这块的概念不是太熟同学,我们还是先看一个小例子。

比如下面这段代码:

转换成AST的话,差不多是下面这样的:

实际转换产物很长,我们只截取一部分,不过可以大致看出AST的整个结构,可以看出,整个AST实际上是对源码的一个树状结构描述,整个结构里包含很多节点对象,每个节点下面又包含各种树形和子节点。

我们将100+的语法节点分类抽象为标识符、字面量、表达式、语法块,其它五大类,30+种的常用节点,同时剥离了与Fair产物解析无关的信息,只保留原始node中的关键信息,使得节点解析更加清晰。

节点类型很多,完整列表可以参考这里。

前面我们说到analyzer的praseFile方法解析完dart源文件后返回了AstNode实例,我们对AstNode的分析主要由该类的accept方法提供,这里用到了访问者设计模式。

accept方法接收的参数类型是AstVisitor,这是一个接口,我们正是通过这个接口的一系列方法实现对上述实例中各个节点的遍历的。

正如上面的例子看到的,原始AstNode数据量很大,哪怕是一个简单的Demo,解析出来的AST实际上是包含很多节点信息的,所以我们并不通过实现AstVisitor接口来实现所有节点类型的访问。analayzer包提供了SimpleAstVisitor,我们可以继承这个类来自定义Visitor,按需要选择我们支持的节点去实现方法就可以了。相关代码如下:

最后返回的是一个Map类型的Ast节点树,感兴趣的同学可以直接通过源码了解细节。

DSL生成

从AST到DSL生成流程

有了第一步生成的AST语法树Map产物,再根据AST Map来生成DSL就比较好理解了。在DSL的生成流程当中,主要是对节点的遍历,然后针对方法,表达式和变量的处理。

因为DSL主要处理的是布局动态化的部分,实际上对于一个Wiget的解析处理,我们主要是针对build方法中return的内容部分进行了提取并生成DSL(此处的methodMap,我们先放到后面再讲解,并不影响对主流程的理解)。相应代码如下所示:

对于DSL的格式,在debug环境,为了方便调试与直观的发现问题,我们的产物采用json格式,在线上环境,处于产物大小的控制及解析速度的考虑,我们下发产物格式改为flatbuffer(google推出的一种高性能,小体积的序列化方案)。

fair布局动态化的大致原理

实际上有了analyzer作为基础,DSL的生成在技术上的难点并不大,可是我们的DSL的结构应该是什么样的,这取决于fair在运行时怎么对DSL进行还原,毕竟我们的DSL生成最后是为了动态还原成Wiget树并渲染的。针对这部分内容,我们做个大致的了解,这样能更好的理解下面的内容。

我们知道,Flutter因为某些原因,对dart:mirror包进行了移除,这就决定了我们没法通过反射对DSL进行布局构建还原,不过Flutter还有一个万能的方法Function.apply。

这个方法,是我们动态化方案中的第二个重要方法,是Widget树还原的基础。

端上接收到下发的DSL后,只能解析到对应的字符串String类型,我们只需将对应的String映射到对应的方法(此处主要支持构造方法和类静态方法),便可以将对应的DSL还原并构建Widget树。在fair当中,大致是这样的。此处我们写了一个工具库,以方便对flutter widget映射关系的自动生成。

DSL结构

了解了fair对DSL解析执行的大致原理之后,我们再来理解DSL的大致结构就比较容易了。

上面的className对应的是上面映射关系中的key,na和pa对应的是可选命名参数和位参数,此处我们需要解释的是上面提到的methodMap

所谓的methodMap,从字面意思上理解,其实就是方法缓存,在这里我们同样以上面的HelloWord类为例

可以看到,在我们的示例中build方法嵌套了一个布局构建方法_buildText()。前面我们讲到,在布局动态化DSL生成过程中,我们主要是对build方法进行了提取,对于这种方法嵌套的布局构建代码,我们该怎么处理呢。大致的应对方法有几种:1.在框架层面做限制,不支持这种写法;2.解析时提取嵌套方法返回的Widget内容,在开发时支持方法嵌套,实际生成DSL时变成纯Widget嵌套方式;3.缓存嵌套的Widget构建方法,在运行时解析Json内容后对函数进行实时的替换。以上方式中,显然1是让人不可接受的,如果要接入动态化框架有这样的限制,恐怕会让使用的开发者望而却步。至于方法2和方法3,其实大同小异,一个是在解析时替换,一个是在运行时替换,考虑到我们生成DSL尽量不要改变原有的代码结构,我们选择了方案3。这就是为什么我们的DSL Json中需要有methodMap的原因。

以上面HelloWord类DSL解析结果为例,可以看到methodMap当中实际上缓存的是除build方法外的Widget构建的相关方法。

变量和表达式的处理

上面我们主要讲的都是方法的处理,但是对于变量和表达式,比如下面这种:

Text(\'$_counter\')

以及这种:

onPressed: _incrementCounter

这两种类型的变量引用,在AST中实际上对应的不同的类型,但是如果在DSL中我们还是简单的处理成了字符串,实际在DSL解析时就无法与普通字符串进行区分了,这里我们采取了一种比较简单的方式,在通过添加不同的特殊符号前缀进行了区分。

例如上面两个示例:

Text(\'$_counter\') => #($_counter)
onPressed: _incrementCounter => @(incrementCounter)

然后在Fair解析支持层面通过正则匹配到不同的表达式类型,来做变量的数据绑定已经方法的逻辑调用等。

总结

整个DSL生成流程细节很多,但是总结下来就是通过analyzert提供的AST解析工具提取并精炼对我们有用的信息,并且根据我们的Fair框架需要,组合成抽象化的布局DSL结构信息。

最后,我们借用Flutter动态化框架Fair的设计与思考中的关于DSL生成流程的一副图来总结一下详细的流程。

可以看到,我们整个的生成流程包含的细节其实很多,但是受限于篇幅,我们无法一一展开来讲,感兴趣的同学可以仔细研究我们的开源代码,也欢迎给我们提出更多的问题和意见,或者直接参与到Fair的开发中来,帮助Fair项目变的更好。

上次编辑于:
贡献者: sunzhe03