Flutter 代码生成 source_gen 使用与原理分析

日常开发中,我们可能会涉及一些重复、模板性的代码工作,比如,数据模型的 fromJson/toJson 方法。这一类生成规则清晰,可以模板化的代码,如果每次都要手写非常不利于摸鱼,毕竟「划水一时爽,一直划水一直爽 」,一个好的摸鱼工具对于打工人至关重要。Dart 中提供 source_gen 工具帮助我们通过脚本自动完成这类工作。本期简单聊聊这个工具的使用,并详细的分析它的构建原理。 (如果你有接触过过 source_gen,可以直接跳到 part3 )


一、source_gen 是什么 —— 代码处理脚本

在我看来,source_gen 本质是一种 Dart 侧的代码处理脚本(类似 Java-APT),它可以提供一个 访问当前仓库中所有代码文件的入口。基于此我们可以通过一些标识,例如 注解(最常见)、类名 等实现一些特定的操作,例如 代码生成,统计 等。

从代码编译流程上看,作用于代码的 编辑阶段,所以你能直接查看到生成的代码。

image.png

下面看个具体例子。


二、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 {}

复制代码

更详情的流程推荐 Flutter 注解处理及代码生成详解Dart中如何通过注解生成代码

下面我们通过 json_serializable 来看看 source_gen 的执行流程。


四、source_gen 执行原理

我们在项目中引入了 json_serializable 之后,执行

flutter packages pub run build_runner build 
复制代码

进入到 json_serializable 的处理流程中,如果我们有其他的脚本程序,也会分别执行。也就是说 source_gen 会搜索整个项目中依赖的脚本,并将当前项目的文件传入。

整个流程如下:

image.png

关键流程有三步,分别对应指令中的

  • 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 命令出发。

image.png

根据我们在环境变量中配置的 sdk 位置,可以找到 flutter 指令对应的脚本程序,最终是由 dart 去执行$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot 对应的程序。这个程序是是由 flutter_tools 项目编译出的产物,等价于 tools/bin 目录下的 flutter_tools.dart,我们可以直接从这里执行程序。

image.png

所以从这个入口相当于参数变成了 packages pub run build_runner build

executable.main(args) 如下

image.png

里面采用 命令模式 配置了多种 command,根据我们传递参数的不同,最终选择不同的 command 执行。

packages pub 最终找到 PackagesPassthroughCommand 执行命令,处理完后参数只剩下 run build_runner build

image.png

这个方法内部通过开启一个新的进程,将参数 run build_runner build 传递过去,执行 dart-sdk 目录下的 bin/pub 程序,两个进程之间通过 socket 通信。

image.png

总结:指令第一段 flutter packages pub 最终会在 flutter_tools 中为我们找到对应的 command 执行。开启一个新的进程,将剩余的参数传递执行。

2、run build_runner build

bin/pub 中也是通过 dart 命令去执行 pub.dart.snapshot 程序。

image.png

这个程序源码在 dart-sdk 中,简单来说,它封装了 pub 仓库的交互指令。dart run build_runner build 会找到 build_runner 中,最终剩下 build 参数。 image.png

main 方法最终会走到 generateAndRun ,里面主要有三步

  • a、findBuildScriptOptions 查找项目依赖中的所有包含 build.yaml 的库
  • b、在项目中生成 entrypoint/build.dart 入口文件,引入所有脚本
  • c、创建 isolate 执行 build.dart

a、findBuildScriptOptions() 查找和生成脚本信息

image.png

方法首先调用 findBuildScriptOptions 查找项目中的所有所有的依赖,并根据依赖关系进行排序,最后查找依赖中所有包含 build.yaml 文件的库。

image.png

b、根据脚本信息在项目中生成入口配置文件

根据上一步找到的信息拼接成字符串,并进行 format

image.png

字符串在项目的 .dart_tools/build/entrypoint/build.dart 位置写入文件。

image.png

这儿便作为项目中所有依赖脚本的入口文件,

image.png

c、创建 isolate 执行 build.dart

在生成好入口文件之后,build_runner 中会创建一个新的 isolate,执行上面的入口程序。两个程序之间通过 ReceivePort 进行通信。

image.png

总结:dart run build_runner build 会找到 build_runner 执行,扫描项目下所有的脚本程序,生成一个 build.dart 的入口程序,之后创建 isolate 执行脚本。

3、实际执行到脚本程序 json_serializable 中

到执行这里比较清晰了,最终肯定是调用到编写的脚本程序中,这段调用关系比较复杂,下方是整个时序图。

image.png

调用比较深,个人认为理解下最后 source_gen 部分就行。

image.png

这里遍历 generators 调用 generate(libraryReader, buildStep),其中 generators 就是我们前面配置的所有脚本程序。

libraryReader 是当前项目代码的集合,包含了所有的代码信息。也是基于 analyzer 将 dart 代码转换成为 AST(abstract syntax tree),借此可以访问到所有类信息,进行我们的自定义操作,例如代码生成/统计等。

根据 gen.generate 的结果,如果不为空则写入到文件中。

image.png

总结:build.dart 中配置了当前依赖的全部脚本程序,从 main 方法开始,执行到每一个脚本程序。根据返回结果,判断是否新生成文件。

到这整个流程差不多分析完了,里面其实还有一些细节可以挖,比如 flutter_tools 的命令模式、dart-sdk 的执行原理、Dart 虚拟机与 isolate 等等,如果大家感兴趣可以再开几期详细聊一聊。

当然,从代码生成的角度来看核心流程还是这张图中,理解之后应该会有更深的感触。

image.png


五、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 进阶优化

Flutter核心渲染机制

Flutter路由设计与源码解析

下期预告:

大概是 flutter_tools/ source_gen 优化 / Dart VM 主题

Guess you like

Origin juejin.im/post/7035784241665277959