阅读 144

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!');   } } 复制代码


二、技术对比

看起来这两项技术都具备代码生成的能力,那么他们有什么差异?

从整个代码从编写到运行的生命周期来看:

image.png

APT 技术作用于代码编辑阶段,执行命令之后,他会将当前仓库的依赖中所有的 source_gen 的执行脚本筛选出来。将当前 package 中的代码封装成一个入口,提交给所有的代码处理程序。生成的内容是在编辑阶段可见的,例如上方的解析代码会生成一个额外的新文件:

image.png

而 AOP 技术作用于产物编译阶段,修改了 flutter_tools 原有的编译流程。他的输入是整个 Flutter 代码在 frontend_server 编译出来的 dill 文件。通过 AOP 处理程序进行二次加工,将新生成的 dill 文件继续执行后续编译流程。

(图片引用自:Flutter 无埋点SDK实现)

因为 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 可以参考:Flutter 注解处理及代码生成,AOP:Flutter 无埋点SDK实现  。

但其实这两项技术仍然有缺点,比如 APT 生成的代码时间很长,并且调试复杂。AOP 热重载会失效等。我们能否研究其关键设计,解决这些问题? 这一小节会去分析下他们的核心流程,我们先抛开所有的方案,想想代码生成是怎么一回事。

1、代码生成本质是什么?

忽略掉技术细节,代码生成其实可以理解为:「一份源代码,通过处理器,生成了一份新的代码」

image.png

其实就三个关键点:「输入,处理,和输出」,这也是区别不同方案的本质。

其实最简单直接的,可以通过一个 python 或者 shell 脚本扫描字符串标识,例如,遍历工程文件中带有 @Test 去操作文件。

这样做优点在于通用性:通过字符串匹配,在任何语言上都可以用类似的思路去做。

缺点也很明显,因为和语言无关,所以在某种具体语言上需要写很多逻辑去识别词法/语法。

而 APT 和 AOP 等第三方库正是根据语言的编写规则,为我们识别了其中的内容,例如:Class,Field,Construcor 等,以此便捷地访问代码。

2、APT(source_gen)的工作流程

APT 的工作流程可以沿着写处理器的实现上梳理。一般我们会继承 GeneratorForAnnotation<T> ,其中 <T> 表示这个处理器要查找的注解。之后重写对应的生成方法,例如直接返回一个 hello world

image.png

这个方法中的 Element 表示 Class 或 Mixin 的元素,以此可以获取所有被 @RouteMap 注解的 Class 或者 Mixin 中所有属性、方法、构造函数等。

image.png

这就是 APT(source_gen)中对于源码进行词法/语法分析之后的结果,便于我们直接操作代码。

向上查看 GeneratorForAnnotation<T> 中可以发现:

image.png

generate(LibraryReader library, BuildStep buildStep) 方法中,会给出一个 libary。一个 libary 表示一个代码文件。之后通过泛型 T 来筛选所有包含此注解的 Element 传递给子类进行处理。(所以这项技术其实不用注解也行,因为能访问到具体 Element 对象

之后根据子类返回的字符串结果,写入一个新的文件。

那么,源码怎么组织成 libary 这个结构,肯定经过词法/语法分析。

在整个 APT(source_gen)的调用过程中,有这么一个节点

image.png

其中 buildStep.inputLibrary 会调用 resolver 去对 inputId 做解析,返回一个 LibaryElement

image.png

resolver 会对每个 AssetId 创建 LibaryElement 文件。

image.png

AssetId 对应的是项目中的一个个独立文件路径,通过最后一行的 drvier 生成,里面对文件进行词法和语法分析。核心的流程都在  dart/analyzer 中。

完整的流程可以参考:Flutter 代码生成 source_gen 使用与原理分析

3、AOP(AspectD)的工作流程

AOP 的细节较多,需要一些 AST 知识,本篇只做主流程梳理,后续开个系列细细分析。

Aop 是在 flutter 产物构建过程,当 font_server 编译结束后会生成一个 dill 文件(理解为安卓中的字节码),通过修改 flutter_tools 执行 AspectD 中的代码对原有的产物处理并进行替换。

image.png

通过 processManager 执行到 AspectD 中

image.png

主要步骤如下

    /// 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 中使用的是 ExpressionStatement 构建代码逻辑,放在下个系列更深入的学习。

最后将修改过的 component 写入到 dill 文件即可。


五、总结

文章重点回顾:

1、代码编辑生命周期以及代码生成方案

image.png

2、两项技术对比,借此选择适合解决问题的方案。

对比项APT(source_gen)AOP(AspectD)
作用阶段代码编辑阶段完整的产物构建阶段
输入当前单个 package所有代码编译出的 dill 文件
输出新文件或者不输出新的 dill 文件
生成代码是否可见可见不可见
是否需要修改 flutter_tools
hot_reload 后是否有效
能否修改 SDK

3、代码生成的本质

image.png


作者:Nayuta
链接:https://juejin.cn/post/7062319340464373791


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐