Flutter 动态化 - Fair 4.0编译原理及优化

1,006 阅读4分钟

前言

Fair 团队在今年6月份发布了Fair 4.0。本次更新可谓是重磅级,其中最重要的特性包括:

  • 扩展FunctionDomain,支持布局回调。开发者不用通过FairDelegate 的方式去实现布局回调,从而大大增强了Fair 的动态化能力。
  • fair_version 的全量映射,并且开源了简单易用的脚本,从此开发者可以不用依赖Fair 团队,自行适配Flutter 版本。

此外还扩充了大量基础语法糖,进一步增强了布局动态化能力。欢迎大家升级体验~

Fair 4.0 Github

Fair 编译原理

Fair 的动态化实现原理包括布局动态化和逻辑动态化,布局动态化保证加载性能的前提下,做到了布局的动态下发,类似于阿里的Tangram;逻辑动态化是对布局动态化的补充,是把业务逻辑以JS 的形式下发到客户端由JSCore/V8解释执行。

在编译期,Fair Compiler会将Dart 代码中的布局部分(主要是build 函数及包含布局逻辑的布局子函数)编译成DSL (json/bin),逻辑部分编译成JS 文件。

Fair Compiler的Dart2Dsl 工具将Dart 源代码转化成Fair DSL,前提是要获取到Dart 文件中的所有信息,这里我们借助了官方提供的 analyzer 工具包获取到完整的抽象语法树信息。

Fair 是如何找到目标Dart 文件完成编译的呢?这里我们借助 source_gen,一个用于 Dart 代码自动生成的工具库。开发者通过注解标记目标Dart 文件,编译器通过 @FairPatch 过滤查找Dart 源代码,从而完成编译。

Fair 编译流程

Fair 4.0之前,Fair Comipler 的编译流程如下:

  • 在BundleBuilder 中,标注@FairPatch 注解的目标Dart 文件,被copy 到工作目录,然后执行Dart2Dsl。
  • 在ArchiveBuilder 中,首先执行Dart2Js 编译得到JS 文件,然后将Dart2Dsl 的编译结果JSON 文件通过FlatBuffersCompiler 编译成Bin 文件,最后Zip 压缩,得到最终产物 FairBundle。

通过上述图中可以看出,单个目标Dart 文件的编译过程是:Copy -> Dart2Dsl -> Dart2Js -> Zip -> Delete Copy。细心的小伙伴会发现一个问题,每一个Dart 文件编译完成之后都会对Bundle 目录执行一次Zip 压缩,这其实是不合理的。

Fair 4.0 编译优化

Fair 4.0 之前的版本中,Fair Compiler 使用analyzer 获取抽象语法树信息的方式如下:

var parseResult = parseFile(
            path: path, featureSet: FeatureSet.latestLanguageVersion());
var compilationUnit = parseResult.unit;
//遍历AST
var astData = compilationUnit.accept(
    CustomAstVisitor(sourcePath, await File(path).readAsString()));

Fair 4.0 版本,增加了对布局回调的支持,需要获取到布局函数更详细的信息,原有的parseFile() 函数已经不能满足需求。因此Fair 4.0 中改用AnalysisContextCollection 实现对抽象语法树信息的全量获取解析。核心代码如下:

var collection = AnalysisContextCollection(includedPaths: [path]);
// 为了获取 Function 的描述,改用该方法
var parseResult = await collection
    .contextFor(path)
    .currentSession
    .getResolvedUnit(path) as ResolvedUnitResult;

var compilationUnit = parseResult.unit;
//遍历AST
var astData = compilationUnit.accept(
    CustomAstVisitor(sourcePath, await File(path).readAsString()));

Fair 4.0 发布前我们发现了一个问题,编译速度明显变慢。以编译Fair Example 工程为例,Fair 4.0 之前编译时间大概是20s,Fair 4.0 编译时间是2min。编译时间变长的原因也不难理解,相较于parseFile(),AnalysisContextCollection 方式获取的是全量的抽象语法树信息,包括父类及祖先结点的相关信息。

有没有优化的空间呢?经过我们调研发现,AnalysisContextCollection 方式,其实是有缓存的,所以对于多文件解析,推荐用法是传入目标文件目录,具体实现方式如下:

Future<String> parseDir(String dir) async {
  var collection = AnalysisContextCollection(includedPaths: <String>[dir]);
  for (final context in collection.contexts) {
    for (final file in context.contextRoot.analyzedFiles()) {
      if (file.endsWith('.dart')) {
        var resolvedUnitResult = await context.currentSession
            .getResolvedUnit(file) as ResolvedUnitResult;
        var ast = await generateAstMapByCompilation(
            resolvedUnitResult.unit, file,
            sourcePath: file.replaceFirst(dir, '').replaceFirst('/', ''));
        var result = fairDsl(ast);
        ......
      }
    }
  }
}

前面介绍过Fair 4.0之前的编译流程,Fair Compiler 都是逐个执行单个目标Dart 的编译。因此需要优化这个编译流程,优化之后的编译流程如下:

优化之后,Fair 4.0的编译时间由优化前的2min 缩短至20s,大大提升了编译速度。

总结

本文主要对比Fair 4.0 及之前版本的编译流程,详细介绍了Fair 4.0的编译原理及优化过程。感兴趣的小伙伴可以通过源码详细了解这部分的实现方案。欢迎大家使用 Fair,也欢迎大家为我们点亮Star。

Github地址:github.com/wuba/fair

Fair官网:fair.58.com