Flutter 代码生成技术 [APT 与 AOP] 适用场景与对比
日常开发中,你是否遇到过一些重复、模板性的代码,比如,数据模型的
fromJson/toJson
方法、或者统计每一个方法的执行时间。这一类代码没有什么难度且琐碎,导致我们加班,不能愉快的摸鱼。好消息是,Flutter 中也有类似原生 APT 和 AOP 的技术。他们有什么特点?该使用何种方案?它们是如何生成代码?且往下看
一、什么是 APT 和 AOP
1、APT(Annotation Processing Tool)注解处理工具
Flutter 中的 APT 一般指 source_gen,通过自定义的注解与处理程序,在 代码编辑阶段 生成对应可见的代码。
基于此的应用 比如:json_serializable,这是一个很好用的 json 解析代码库,通过给 model 添加 @JsonClass
注解,可以自动为我们生成对应的 fromJson/toJson
方法,并且支持属性别名以及复杂的 List 结构。例如:
源代码:
@JsonClass()
class CityModel {
@JsonField(["cityCode"])
String cityCode;
@JsonField(["cityName"])
String cityName;
CityModel();
factory CityModel.fromJson(Map<String, dynamic> json) {
return _$CityModelFromJson(json);
}
Map<String, dynamic> toJson() {
return _$CityModelToJson(this);
}
}
执行
flutter packages pub run build_runner build --delete-conflicting-outputs
自动生成对应的解析方法 :
CityModel _$CityModelFromJson(Map<String, dynamic> json) {
CityModel instance = CityModel();
instance.cityCode =
parseFieldByType<String>(json['cityCode'], instance.cityCode);
instance.cityName =
parseFieldByType<String>(json['cityName'], instance.cityName);
return instance;
}
Map<String, dynamic> _$CityModelToJson(CityModel instance) => <String, dynamic>{
'cityCode': instance.cityCode,
'cityName': instance.cityName,
};
2、AOP(Aspect-Oriented Programming)面向切面编程
Flutter 中的 APT 一般指 aspectd。可以在任意地方,通过 PointCut 在 Flutter 产物构建阶段 插入指定的代码。
参考 aspectd 中的 demo,通过 @Execute
注解,应用打包运行。在 _MyHomePageState
调用 _incrementCounter
的时候输出 KWLM called!
。
import 'package:aspectd/aspectd.dart';
@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {
@pragma("vm:entry-point")
ExecuteDemo();
@Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter")
@pragma("vm:entry-point")
void _incrementCounter(PointCut pointcut) {
pointcut.proceed();
print('KWLM called!');
}
}
二、技术对比
看起来这两项技术都具备代码生成的能力,那么他们有什么差异?
从整个代码从编写到运行的生命周期来看:
APT 技术作用于代码编辑阶段,执行命令之后,他会将当前仓库的依赖中所有的 source_gen
的执行脚本筛选出来。将当前 package 中的代码封装成一个入口,提交给所有的代码处理程序。生成的内容是在编辑阶段可见的,例如上方的解析代码会生成一个额外的新文件:
而 AOP 技术作用于产物编译阶段,修改了 flutter_tools 原有的编译流程。他的输入是整个 Flutter 代码在 frontend_server 编译出来的 dill 文件。通过 AOP 处理程序进行二次加工,将新生成的 dill 文件继续执行后续编译流程。
因为 dill 产物中会包含所有的 flutter 代码,所以 AOP 可以在任何地方生成代码,包括 Flutter/Dart SDK 中。
两项技术对比来看:
对比项 | APT(source_gen) | AOP(AspectD) |
---|---|---|
作用阶段 | 代码编辑阶段 | 完整的产物构建阶段 |
输入 | 当前单个 package | 所有代码编译出的 dill 文件 |
输出 | 新文件或者不输出 | 新的 dill 文件 |
生成代码是否可见 | 可见 | 不可见 |
是否需要修改 flutter_tools | 否 | 是 |
hot_reload 后是否有效 | 是 | 否 |
能否修改 SDK | 否 | 是 |
三、什么时候使用 APT/AOP ?
通过上面的认知,我们会发现两项技术都能做到代码生成,那如何判断该用哪种呢? 通过两个场景来分析:
1、Json 解析:APT 而不用 AOP
上面提到的 json 注解生成是基于 APT,其实 AOP 方案也能做,并且相对来说更加优雅(因为不影响业务工程)。但它 最大的问题在于 AOP 生成的代码在 hot reload/restart 之后就擦除了。
前面我们提到过,AOP 方案在 完整的产物构建阶段 执行,但是热重载并不走这个流程。当重载推入新的产物到 APP 时,并没有 AOP 生成的内容。意味着如果使用 AOP 方案,需要每次打包构建 APP 产物,失去热重载带来的高效开发能力。
而 APT 生成的代码是编辑阶段可见的,其实和手写代码并没有区别。APT 只是帮助我们省去了这部分工作,所以热重载之后同样生效。
2、统计方法耗时:AOP 而不用 APT
比如,我们想要统计所有代码中的统计耗时,其实就是在方法调用前后,插入统计代码,理论上 APT 也可以做到。但是 APT 有几个限制:
-
APT 只能基于当前的 package 生成代码,无法在第三方依赖中插入。
-
APT 会显式的侵入业务层插入代码。
这时 AOP 方案的优势便凸显出来,可以在业务代码无感的情况下生成代码,并且覆盖第三方甚至 SDK 中的代码。
所以该选择哪种方案可以参考两种对比,结合实际需求进行选择。
四、代码生成核心流程分析
但其实这两项技术仍然有缺点,比如 APT 生成的代码时间很长,并且调试复杂。AOP 热重载会失效等。我们能否研究其关键设计,解决这些问题? 这一小节会去分析下他们的核心流程,我们先抛开所有的方案,想想代码生成是怎么一回事。
1、代码生成本质是什么?
忽略掉技术细节,代码生成其实可以理解为:「一份源代码,通过处理器,生成了一份新的代码」
其实就三个关键点:「输入,处理,和输出」,这也是区别不同方案的本质。
其实最简单直接的,可以通过一个 python 或者 shell 脚本扫描字符串标识,例如,遍历工程文件中带有 @Test
去操作文件。
这样做优点在于通用性:通过字符串匹配,在任何语言上都可以用类似的思路去做。
缺点也很明显,因为和语言无关,所以在某种具体语言上需要写很多逻辑去识别词法/语法。
而 APT 和 AOP 等第三方库正是根据语言的编写规则,为我们识别了其中的内容,例如:Class,Field,Construcor 等,以此便捷地访问代码。
2、APT(source_gen)的工作流程
APT 的工作流程可以沿着写处理器的实现上梳理。一般我们会继承 GeneratorForAnnotation<T>
,其中 <T> 表示这个处理器要查找的注解。之后重写对应的生成方法,例如直接返回一个 hello world
。
这个方法中的 Element 表示 Class 或 Mixin 的元素,以此可以获取所有被 @RouteMap
注解的 Class 或者 Mixin 中所有属性、方法、构造函数等。
这就是 APT(source_gen)中对于源码进行词法/语法分析之后的结果,便于我们直接操作代码。
向上查看 GeneratorForAnnotation<T>
中可以发现:
在 generate(LibraryReader library, BuildStep buildStep)
方法中,会给出一个 libary。一个 libary 表示一个代码文件。之后通过泛型 T 来筛选所有包含此注解的 Element 传递给子类进行处理。(所以这项技术其实不用注解也行,因为能访问到具体 Element 对象)
之后根据子类返回的字符串结果,写入一个新的文件。
那么,源码怎么组织成 libary 这个结构,肯定经过词法/语法分析。
在整个 APT(source_gen)的调用过程中,有这么一个节点
其中 buildStep.inputLibrary
会调用 resolver
去对 inputId
做解析,返回一个 LibaryElement
。
resolver
会对每个 AssetId
创建 LibaryElement
文件。
AssetId
对应的是项目中的一个个独立文件路径,通过最后一行的 drvier 生成,里面对文件进行词法和语法分析。核心的流程都在 dart/analyzer
中。
3、AOP(AspectD)的工作流程
AOP 的细节较多,需要一些 AST 知识,本篇只做主流程梳理,后续开个系列细细分析。
Aop 是在 flutter 产物构建过程,当 font_server 编译结束后会生成一个 dill 文件(理解为安卓中的字节码),通过修改 flutter_tools 执行 AspectD 中的代码对原有的产物处理并进行替换。
通过 processManager 执行到 AspectD 中
主要步骤如下
/// 1、读取 dill 文件
final Component component = dillOps.readComponentFromDill(intputDill);
/// 2、解析项目所有依赖中包含 Aspect 注解的程序
_resolveAopProcedures(libraries);
///************* 省去诸多代码
/// 3、根据上一步检索的结果执行 Execute/Inject 等注解的代码生成
/// Aop execute transformer
if (executeInfoList.isNotEmpty) {
AopExecuteImplTransformer(executeInfoList, libraryMap)..aopTransform();
}
/// Aop inject transformer
if (injectInfoList.isNotEmpty) {
AopInjectImplTransformer(injectInfoList, libraryMap, concatUriToSource)
..aopTransform();
}
/// 将处理过的 component 对象重新写入到之前的 dill 路径
dillOps.writeDillFile(component, outputDill);
第二步中如何查找 @Aspect
的注解处理程序,跟踪源码其实会发现,就是通过遍历所有的文件,通过字符匹配获取到 @Execute
@Inject
等注解程序对应的代码,之后存入集合执行。
/// 通过字符串匹配,获取定义的 AOP 类型
static AopMode getAopModeByNameAndImportUri(String name, String importUri) {
///*** 省略类似代码
if (name == kAopAnnotationClassExecute &&
importUri == kImportUriAopExecute) {
return AopMode.Execute;
}
if (name == kAopAnnotationClassInject && importUri == kImportUriAopInject) {
return AopMode.Inject;
}
///****
}
不过这里代码生成的方法和 APT 不同,上面 source_gen 是通过 字符串 生成代码,而 Apsectd 中使用的是 Expression
、Statement
构建代码逻辑,放在下个系列更深入的学习。
最后将修改过的 component
写入到 dill 文件即可。
五、总结
文章重点回顾:
1、代码编辑生命周期以及代码生成方案
2、两项技术对比,借此选择适合解决问题的方案。
对比项 | APT(source_gen) | AOP(AspectD) |
---|---|---|
作用阶段 | 代码编辑阶段 | 完整的产物构建阶段 |
输入 | 当前单个 package | 所有代码编译出的 dill 文件 |
输出 | 新文件或者不输出 | 新的 dill 文件 |
生成代码是否可见 | 可见 | 不可见 |
是否需要修改 flutter_tools | 否 | 是 |
hot_reload 后是否有效 | 是 | 否 |
能否修改 SDK | 否 | 是 |
3、代码生成的本质
4、核心的 API
- 通过路径解析代码:LibraryElement element = Resolver.libraryFor(path)
- 解析 dill 文件: Component component = dillOps.readComponentFromDill(intputDill);
六、下期预告
下个系列会从 使用-源码 分析,深入浅出和大家一起学习 Flutter 中虚拟机和事件机制。可能会以专栏或者小册的形式发布,敬请期待。
ps:下个系列挑战不小,会闭关一段时间,希望能在一个月内有所突破。
文章首发于我的公众号:进击的Flutter 或者 runflutter ,里面整理收集了最详细的 Flutter 进阶与优化指南,欢迎关注。
如果你有任何疑问可以通过公众号与联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~
往期精彩内容:
转载自:https://juejin.cn/post/7062319340464373791