Flutter 代码生成 source_gen 使用与原理分析
日常开发中,我们可能会涉及一些重复、模板性的代码工作,比如,数据模型的 fromJson/toJson
方法。这一类生成规则清晰,可以模板化的代码,如果每次都要手写非常不利于摸鱼,毕竟「划水一时爽,一直划水一直爽 」,一个好的摸鱼工具对于打工人至关重要。Dart 中提供 source_gen 工具帮助我们通过脚本自动完成这类工作。本期简单聊聊这个工具的使用,并详细的分析它的构建原理。
(如果你有接触过过 source_gen,可以直接跳到 part3 )
一、source_gen 是什么 —— 代码处理脚本
在我看来,source_gen 本质是一种 Dart 侧的代码处理脚本(类似 Java-APT),它可以提供一个 访问当前仓库中所有代码文件的入口。基于此我们可以通过一些标识,例如 注解(最常见)、类名 等实现一些特定的操作,例如 代码生成,统计 等。
从代码编译流程上看,作用于代码的 编辑阶段,所以你能直接查看到生成的代码。
下面看个具体例子。
二、source_gen 能做什么 —— json 解析
最典型的我觉得是 json 解析:json_serializable
这是一个很好用的 json 解析生成代码库,通过给 model 添加注解,可以自动为我们生成对应的 fromJson/toJson
方法。
import 'package:json_annotation/json_annotation.dart';
part 'example.g.dart';
/// 注解标识这是一个 model 对象
@JsonSerializable()
class Person {
final String name;
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
执行脚本后,自动生成一个新的文件,里面包含以下生成代码:
part of 'example.dart';
Person _$PersonFromJson(Map<String, dynamic> json) => Person(
name: json['firstName'] as String,
);
Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
'name': instance.firstName,
};
这种解析方式对比拿 json 数据生成整个 model 的好处在于:
- 后期字段 增/删 方便, 可以直接改 model 文件,再执行脚本生成解析方法,避免修改都要重新生成整个 model。
- model 可以进行复用,一些已有 model 可以作为字段写到其他 model 中。
当然前面我们也提到过,它只是提供一个遍历工程代码文件的入口,所以能做的东西完全取决于你的实现。
三、source_gen 怎么用 —— 开发脚本与运行
对于 source_gen 开发已有很多介绍了,这里因为下面的流程分析需要,我们以一个注解处理程序,简单看看关键步骤。
1、新建 package,创建你的注解,以及注解处理程序
例如,创建一个 TestMetadata
的注解:
class TestMetadata {
const TestMetadata();
}
之后创建一个注解的处理器 Generator,继承于 GeneratorForAnnotation。
实现 generateForAnnotatedElement
方法。这个方法中会自动给我们筛选出 项目中所有带有这个注解的类 Element,你可以拿到它们执行任何你需要的逻辑。(能获取到项目中的所有类,只是继承于 GeneratorForAnnotation 会自动为我们筛选带有注解的类)
class TestGenerator extends GeneratorForAnnotation<TestMetadata> {
@override
generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
/// 生成以下代码
return "class Tessss{}";
}
}
二、在新建的 package 中配置 build.yaml 文件
builders:
testBuilder:
/// 你的注解程序所处文件
import: "package:flutter_annotation/test.dart"
/// 注解程序对应的构造方法
builder_factories: ["testBuilder"]
/// 生成的新文件后缀
build_extensions: {".dart": [".g.part"]}
auto_apply: root_package
build_to: source
之后在你需要执行的项目中引入这个 package,添加完注解后执行
flutter packages pub run build_runner build
便可以看见根据脚本程序,自动生成的类文件 TestModel.g.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// TestGenerator
// **************************************************************************
class Tessss {}
下面我们通过 json_serializable 来看看 source_gen 的执行流程。
四、source_gen 执行原理
我们在项目中引入了 json_serializable 之后,执行
flutter packages pub run build_runner build
进入到 json_serializable 的处理流程中,如果我们有其他的脚本程序,也会分别执行。也就是说 source_gen 会搜索整个项目中依赖的脚本,并将当前项目的文件传入。
整个流程如下:
关键流程有三步,分别对应指令中的
- 1、flutter packages pub
- 2、run build_runner build
- 3、source_gen 实际执行到 json_serializable 文件
下面我们一步步看:
1、flutter_tools 解析指令 flutter packages pub
因为脚本的是在 flutter packages pub run build_runner build
指令之后开始启动,入口肯定是从 flutter
命令出发。
根据我们在环境变量中配置的 sdk 位置,可以找到 flutter 指令对应的脚本程序,最终是由 dart 去执行$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot
对应的程序。这个程序是是由 flutter_tools 项目编译出的产物,等价于 tools/bin 目录下的 flutter_tools.dart
,我们可以直接从这里执行程序。
所以从这个入口相当于参数变成了 packages pub run build_runner build
executable.main(args)
如下
里面采用 命令模式 配置了多种 command
,根据我们传递参数的不同,最终选择不同的 command 执行。
packages pub
最终找到 PackagesPassthroughCommand
执行命令,处理完后参数只剩下 run build_runner build
。
这个方法内部通过开启一个新的进程,将参数 run build_runner build
传递过去,执行 dart-sdk 目录下的 bin/pub
程序,两个进程之间通过 socket 通信。
总结:指令第一段
flutter packages pub
最终会在 flutter_tools 中为我们找到对应的 command 执行。开启一个新的进程,将剩余的参数传递执行。
2、run build_runner build
bin/pub
中也是通过 dart 命令去执行 pub.dart.snapshot
程序。
这个程序源码在 dart-sdk 中,简单来说,它封装了 pub 仓库的交互指令。dart run build_runner build
会找到 build_runner 中,最终剩下 build
参数。
main 方法最终会走到 generateAndRun
,里面主要有三步
- a、
findBuildScriptOptions
查找项目依赖中的所有包含 build.yaml 的库 - b、在项目中生成
entrypoint/build.dart
入口文件,引入所有脚本 - c、创建 isolate 执行
build.dart
a、findBuildScriptOptions() 查找和生成脚本信息
方法首先调用 findBuildScriptOptions
查找项目中的所有所有的依赖,并根据依赖关系进行排序,最后查找依赖中所有包含 build.yaml 文件的库。
b、根据脚本信息在项目中生成入口配置文件
根据上一步找到的信息拼接成字符串,并进行 format
字符串在项目的 .dart_tools/build/entrypoint/build.dart
位置写入文件。
这儿便作为项目中所有依赖脚本的入口文件,
c、创建 isolate 执行 build.dart
在生成好入口文件之后,build_runner
中会创建一个新的 isolate,执行上面的入口程序。两个程序之间通过 ReceivePort 进行通信。
总结:
dart run build_runner build
会找到 build_runner 执行,扫描项目下所有的脚本程序,生成一个 build.dart 的入口程序,之后创建 isolate 执行脚本。
3、实际执行到脚本程序 json_serializable 中
到执行这里比较清晰了,最终肯定是调用到编写的脚本程序中,这段调用关系比较复杂,下方是整个时序图。
调用比较深,个人认为理解下最后 source_gen 部分就行。
这里遍历 generators 调用 generate(libraryReader, buildStep),其中 generators 就是我们前面配置的所有脚本程序。
libraryReader 是当前项目代码的集合,包含了所有的代码信息。也是基于 analyzer 将 dart 代码转换成为 AST(abstract syntax tree),借此可以访问到所有类信息,进行我们的自定义操作,例如代码生成/统计等。
根据 gen.generate 的结果,如果不为空则写入到文件中。
总结:build.dart 中配置了当前依赖的全部脚本程序,从 main 方法开始,执行到每一个脚本程序。根据返回结果,判断是否新生成文件。
到这整个流程差不多分析完了,里面其实还有一些细节可以挖,比如 flutter_tools 的命令模式、dart-sdk 的执行原理、Dart 虚拟机与 isolate 等等,如果大家感兴趣可以再开几期详细聊一聊。
当然,从代码生成的角度来看核心流程还是这张图中,理解之后应该会有更深的感触。
五、source_gen 代码生成中的问题
最后聊聊我在使用 source_gen 时遇到的问题,这其实也是这篇文章产生的源头。
-
1、调试困难 从上面流程可以看出,整个脚本执行链路非常长,涉及到跨进程和 isolate 通信,这就使得脚本程序的 debug 变得非常困难,只有通过加日志的方式调试。
-
2、执行时间长 还有一点是比如项目中引入了多个脚本,当每次执行命令运行时,都会执行全部的脚本。脚本越多,执行时间越长。目前没有看到能支持指定某个 package 运行,加上难以调试,存在一定的开发成本。
当然了解了整个构建原理之后,这些问题有哪些解决方向大概就有了,敬请期待后续文章~
六、总结
虽然也提了 source_gen 存在的问题,但总的来说,基于它快速开发一些基础脚本例如 json/路由
之类挺不错。并且通过操作 AST 理解代码的的基本编译,也可以作为后续学习一些 动态化/AOP 的实践。里面还有一些细节知识点,比如 flutter_tools 的命令模式、dart-sdk 的执行原理、Dart 虚拟机与 isolate 等等,如果大家感兴趣可以再开几期详细聊一聊。
如果你有任何疑问可以通过公众号与联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~
公众号:进击的Flutter或者 runflutter 里面整理收集了最详细的Flutter进阶与优化指南,欢迎关注。
往期精彩内容:
下期预告:
大概是 flutter_tools/ source_gen 优化 / Dart VM 主题
转载自:https://juejin.cn/post/7035784241665277959