淘先锋技术网

首页 1 2 3 4 5 6 7


前言

开发Flutter项目时遇到了一个奇怪的问题,之所以奇怪是因为源码从Dart语法上来看是没问题的。源码看不出问题,没办法只能尝试看看编译后是否有区别。
从报错信息看应该是Dart后端编译器(backend compiler)报的错,不过看了看Dart SDK源码暂时没什么头绪,于是就想着要不先看看前端编译(front compiler)后的中间表示(或者叫中间代码,IR),也许问题的源头就出在这里。

开发环境

  • macOS: 13.4
  • Dart: 3.0.5

中间表示(IR)

谈到中间表示可能会感觉有点陌生,但是我要说Java的字节码就是一种中间表示,你可能就熟悉起来了。那么在Dart中,中间表示是什么呢?找到Dart编译相关的文档

screenshot1

kernel(内核)就是我们要找的中间表示。和Java的字节码类似,Dart的中间表示也可以在各个平台运行:

screenshot2

当然,这个对于大部分人还是有点陌生,但是说到dill文件,很多人应该不陌生,Flutter项目编译时就会生成app.dill文件(位于项目根目录下的.dart_tool/flutter_build/xxx/app.dill路径)。

dill文件是由中间表示序列化而成的二进制格式文件,本篇文章要做的就是将dill文件反序列化到内存后再重新序列化为文本格式,这里之所以不说dill文件反编译,就是因为从dill文件到文本文件只是反序列化与序列化,并没有涉及到反编译。

中间表示序列化为文本是根据抽象语法树(AST)生成可读的文本格式,不同于抽象语法树的树形数据结构,生成的线性文本更易于查看中间表示的结构和内容。至于为什么是根据抽象语法树生成,请继续往下看。

参考文档:

dill文件生成

执行以下命令会默认在xxx.dart文件同目录下生成同名的xxx.dill文件:

dart compile kernel xxx.dart

也可以通过设置--output参数指定dill文件的路径。如果想了解Flutter项目怎么手动生成app.dill文件请看Dart - dill文件序列化为可读文本(续)

dill文件序列化为可读文本

1. 过时方法

很久以前,刚接触Flutter的时候,尝试过将dill文件序列化为可读文本。那时大概是这样做的:

1.1. 获取完整的Dart SDK

git clone https://github.com/dart-lang/sdk.git

因为后续需要用到sdk/pkg/vm/bin/dump_kernel.dart文件,而Flutter自动下载的Dart SDK没有,所以需要手动获取完整的Dart SDK。

1.2. 切换Dart SDK版本

如果下载的Dart SDK版本和生成dill文件的Dart版本不一致,后续可能会出现以下报错:

Unhandled exception:
Unexpected Kernel Format Version 101 (expected 77)
#0      BinaryBuilder.readComponent.<anonymous closure> (package:kernel/binary/ast_from_binary.dart:640:9)
#1      Timeline.timeSync (dart:developer/timeline.dart:166:22)
#2      BinaryBuilder.readComponent (package:kernel/binary/ast_from_binary.dart:627:21)
#3      main (file:///Users/xxx/sdk/pkg/vm/bin/dump_kernel.dart:54:40)
#4      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:294:33)
#5      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)

解决这个问题的办法有两种,一般选第二种:

  • 一是使用Dart SDK中的dart命令生成dill文件,但是这需要先构建Dart SDK,不然直接执行sdk/sdk/bin/dart命令会报错:
ls: /Users/xxx/sdk/sdk/bin/../../xcodebuild/: No such file or directory
No valid dart configuration found in /Users/xxx/sdk/sdk/bin/../../xcodebuild/
  • 二是切换Dart SDK版本:

[版本名称]可以通过dart --version命令获取。

1.3. 序列化为可读文本

执行以下命令将xxx.dill文件序列化为xxx.txt文件:

dart /xxx/sdk/pkg/vm/bin/dump_kernel.dart xxx.dill xxx.txt

2. 序列化报错

按前面的方法在Dart SDK 3.0.5版本操作,出现报错:

Error: Couldn't resolve the package 'kernel' in 'package:kernel/kernel.dart'.
Error: Couldn't resolve the package 'kernel' in 'package:kernel/binary/ast_from_binary.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/direct_call.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/inferred_type.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/procedure_attributes.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/table_selector.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/unboxing_info.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/unreachable.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/call_site_attributes.dart'.
Error: Couldn't resolve the package 'vm' in 'package:vm/metadata/loading_units.dart'.

用Android Studio打开Dart SDK项目,不出所料一堆报错,不过好像都是依赖库问题导致的未定义报错:

screenshot3

尝试执行dart pub get命令解决依赖问题,结果报错了:

Because vm depends on dart2wasm any which doesn't exist (could not find package dart2wasm at https://pub.flutter-io.cn), version solving failed.

这个报错的意思是在Pub仓库找不到dart2wasm库,仔细看看pkg/vm/pubspec.yaml文件,发现依赖项是这么列的:

# Use 'any' constraints here; we get our versions from the DEPS file.
dependencies:
  args: any
  build_integration: any
  collection: any
  crypto: any
  front_end: any
  kernel: any
  package_config: any
  yaml: any
  
# Use 'any' constraints here; we get our versions from the DEPS file.
dev_dependencies:
  dart2wasm: any
  expect: any
  json_rpc_2: any
  lints: any
  path: any
  test: any
  web_socket_channel: any

没有指定版本号,用的是any。在Pub仓库搜索一番确实不存在dart2wasm库,不过在Dart SDK项目里面找到了(位于pkg/dart2wasm路径)。难道这些依赖库都是存在于Dart SDK项目,那我全部指定为相对路径是不是就可以了?

先改为这样:

# Use 'any' constraints here; we get our versions from the DEPS file.
dev_dependencies:
  dart2wasm:
    path: ../dart2wasm
  ...

重新执行dart pub get命令,确实不再报dart2wasm库的错,但是换成了其他库。如果一个个修改那也太多了,肯定还有其他办法。依赖项前面有一句注释提到了从DEPS文件中获取依赖版本,DEPS文件是什么呢?DEPS文件就是用于描述依赖关系的文件,本质是一个python脚本。关于DEPS文件的一些补充内容请看Dart - dill文件序列化为可读文本(续)

看来不知道是从哪个版本开始更改了依赖管理方式,查看Git历史提交记录,是在2.18.0的dev版本做了改动,所以如果你还在用2.18.0之前的Dart版本,前面的方法应该还是有效的。通过以下命令切换到2.17.7版本:

git checkout 2.17.7

你会发现前面报错的这些库原先就是通过指定相对路径实现依赖的。

3. 新的方法

3.1. 安装python3

需要python环境,如果执行python3命令失败,那么需要先安装python3。可以通过官网下载安装或brew命令安装:

brew install python

安装过程可能会遇到这样的问题:

Error: [email protected]: the bottle needs the Apple Command Line Tools to be installed.
  You can install them, if desired, with:
    xcode-select --install

执行xcode-select --install命令安装Xcode命令行工具解决。

3.2. depot_tools

depot_tools是Chromium的源码管理工具,后续获取源码和管理依赖项都需要用到。

安装流程:

  1. 切换到想要存放的路径执行clone命令(需要代理)
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  1. 配置环境变量

~/.bashrc~/.zshrc文件中加上:

参考文档:

3.3. 获取Dart SDK源码

切换到想要存放Dart SDK源码的位置新建dart-sdk目录:

mkdir dart-sdk

切换到dart-sdk目录下执行命令获取源码(需要代理):

fetch dart

这个操作比较耗时,大概耗费5GB流量以及占用12GB空间。如果将命令改为fetch --no-history dart(仅获取最新源码),实测大概耗费3GB流量以及占用9GB空间。这么一对比,好像是能少下载一些东西,不过不建议,由于只获取最新源码导致后面不好切换版本。

获取源码成功后会自动执行gclient sync命令同步依赖项,如果同步依赖项时遇到错误被中断可以手动执行gclient sync命令继续同步。如果命令执行卡在Updating depot_tools...,请看这篇文章depot_tools问题记录 - 执行fetch/gclient命令无响应

参考文档:

3.4. 切换Dart SDK版本

这时将dill文件序列化为可读文本大概率会因为版本不一致出现前面提到的问题,所以需要先切换Dart SDK版本。切换到dart-sdk/sdk路径下执行:

[版本名称]可以通过dart --version命令获取。切换成功后执行命令同步依赖项:

改变版本后,依赖的第三方库可能会有所删减,如果你不想有未使用的第三方库还残留在项目中,可以将同步依赖项的命令改为gclient sync -D

3.5. 序列化为可读文本

执行以下命令将xxx.dill文件序列化为xxx.txt文件:

dart /xxx/dart-sdk/sdk/pkg/vm/bin/dump_kernel.dart xxx.dill xxx.txt

如果出现这样的提示:

Usage: dump_kernel input.dill output.txt
Dumps kernel binary file with VM-specific metadata.

请检查路径是否有空格,如果有空格请用双引号包裹路径。

3.6. 简单测试

新建一个Dart项目,在项目的lib目录下新建一个main.dart文件,然后往文件中简单写点东西:

class Test {
  void test(List<String> params) {
    for (var param in params) {
      print(param);
    }
  }
}

在项目根路径下执行dart compile kernel lib/main.dart命令,执行成功后lib目录下会生成main.dill文件。

继续执行dart /xxx/dart-sdk/sdk/pkg/vm/bin/dump_kernel.dart lib/main.dill lib/main.txt命令(xxx是需要你自己补全的路径),执行成功后lib目录下会生成main.txt文件。

main.txt

main = <No Member>;
library from "package:untitled/main.dart" as main {

  class Test extends core::Object {
    synthetic constructor •() → main::Test
      : super core::Object::•()
      ;
    method test(core::List<core::String> params) → void {
      {
        synthesized core::Iterator<core::String> :sync-for-iterator = params.{core::Iterable::iterator}{core::Iterator<core::String>};
        for (; :sync-for-iterator.{core::Iterator::moveNext}(){() → core::bool}; ) {
          core::String param = :sync-for-iterator.{core::Iterator::current}{core::String};
          {
            core::print(param);
          }
        }
      }
    }
  }
}

文本内容可读性很高,再加上有源码对照阅读,理解起来还是很容易的。从文本内容可以反推源码中的for-in 循环语法糖大致等价于这段源码:

void test(List<String> params) {
  {
    Iterator<String> iterator = params.iterator;
    for (; iterator.moveNext();) {
      String param = iterator.current;
      {
        print(param);
      }
    }
  }
}

for-in 循环语法糖替换为等价源码后重新生成的main.txt

main = <No Member>;
library from "package:untitled1/main.dart" as main {

  class Test extends core::Object {
    synthetic constructor •() → main::Test
      : super core::Object::•()
      ;
    method test(core::List<core::String> params) → void {
      {
        core::Iterator<core::String> iterator = params.{core::Iterable::iterator}{core::Iterator<core::String>};
        for (; iterator.{core::Iterator::moveNext}(){() → core::bool}; ) {
          core::String param = iterator.{core::Iterator::current}{core::String};
          {
            core::print(param);
          }
        }
      }
    }
  }
}

抽象语法树(AST)

前面提到中间表示序列化为文本是根据抽象语法树(AST)生成可读的文本格式,这句话是有依据的。打开dump_kernel.dart文件:

main(List<String> arguments) async {
  // 必须指定input.dill和output.txt两个参数
  if (arguments.length != 2) {
    print(_usage);
    exit(1);
  }

  final input = arguments[0];
  final output = arguments[1];

  // 抽象语法树的根节点,定义于pkg/kernel/lib/ast.dart
  final component = new Component();

  // Register VM-specific metadata.
  component.addMetadataRepository(new DirectCallMetadataRepository());
  component.addMetadataRepository(new InferredTypeMetadataRepository());
  component.addMetadataRepository(new ProcedureAttributesMetadataRepository());
  component.addMetadataRepository(new TableSelectorMetadataRepository());
  component.addMetadataRepository(new UnboxingInfoMetadataRepository());
  component.addMetadataRepository(new UnreachableNodeMetadataRepository());
  component.addMetadataRepository(new CallSiteAttributesMetadataRepository());
  component.addMetadataRepository(new LoadingUnitsMetadataRepository());

  // 读取dill文件
  final List<int> bytes = new File(input).readAsBytesSync();
  // 将dill文件反序列化为内存中的抽象语法树
  new BinaryBuilderWithMetadata(bytes).readComponent(component);

  // 将内存中的抽象语法树序列化为文本
  writeComponentToText(component, path: output, showMetadata: true);
}

核心方法有两个,一是readComponent,定义于pkg/kernel/lib/binary/ast_from_binary.dart文件,用于dill文件反序列化;二是writeComponentFile,从writeComponentToText方法点进去可以看到,定义于pkg/kernel/lib/text/ast_to_text.dart文件,用于序列化为文本。

关于readComponent方法,这里要提到因为Dart SDK版本不一致导致的报错,抛出这个报错的判断就在这个方法:

List<SubComponentView>? readComponent(Component component,
    {bool checkCanonicalNames = false, bool createView = false}) {
  return Timeline.timeSync<List<SubComponentView>?>(
      "BinaryBuilder.readComponent", () {
    ...
    int version = readUint32();
    if (version != Tag.BinaryFormatVersion) {
      throw InvalidKernelVersionError(filename, version);
    }
    ...
    return views;
  });
}

readUint32()方法用于读取dill文件中的版本,Tag.BinaryFormatVersion是当前的版本,如果不匹配则抛出InvalidKernelVersionError异常,我们看到的报错内容就是来源于这个异常的toString方法:

class InvalidKernelVersionError {
  final String? filename;
  final int version;

  InvalidKernelVersionError(this.filename, this.version);

  
  String toString() {
    StringBuffer sb = new StringBuffer();
    sb.write('Unexpected Kernel Format Version ${version} '
        '(expected ${Tag.BinaryFormatVersion})');
    if (filename != null) {
      sb.write(' when reading $filename.');
    }
    return '$sb';
  }
}

注意,这个版本不是Dart SDK的版本号,而是二进制格式的版本号,例如Dart SDK 3.0.53.0.4版本的Tag.BinaryFormatVersion都是101。简单来说,序列化与反序列化就是按照约定的格式去生成和解析文件,当约定的格式发生变化,用新的格式去解析旧的格式生成的文件大概率是不兼容的,所以设置一个版本号,每当约定的格式发生变化时,版本号递增。

关于writeComponentFile方法建议直接看ast_to_text.dart文件,如果你不太理解序列化的文本内容,那就更建议看这个文件。例如前面文本内容中出现的::,虽然能猜出这个是表示引用,类似于.,但是也不能完全确定,在ast_to_text.dart文件搜索::,根据搜索结果基本能确定下来:

screenshot4

如果你想深入了解关于序列化与反序列化的源码,那么调试源码少不了。调试方法有两种:

  1. Android Studio调试

先用Android Studio打开dart-sdk/sdk目录下的项目,然后配置命令参数,如果路径存在空格,请用双引号包裹路径:

screenshot5

参数配置保存后,和日常开发一样打断点调试就行。

  1. Dart命令调试

原先的命令增加参数(--pause-isolates-on-start --observe)执行后打开链接进行断点调试:

dart --pause-isolates-on-start --observe /xxx/dart-sdk/sdk/pkg/vm/bin/dump_kernel.dart xxx.dill xxx.txt

如果看到这对于怎么调试还不清楚或者遇到问题,可以参考这篇文章Flutter - 命令行工具源码调试环境搭建

最后补充一点,目前只有二进制格式文件支持反序列化为抽象语法树,文本格式是不支持的,所以想通过修改文本内容后重新序列化为dill文件暂时是不行的。

最后

如果这篇文章对你有所帮助,请不要吝啬你的点赞👍加星🌟,谢谢~