怎样的Flutter Engine定制流程,才能实现真正“开箱即用”?

引言

使用Flutter的过程中,如果遇到Flutter Engine的问题需要对其进行修改定制,那么我们需要对它的编译、打包以及发布流程非常清楚。这次在Flutter升级的过程中,发现之前Flutter Engine编译发布的脚本存在不少问题:

  • 没法做到开箱即用

  • 脚本 分散在多个文件中不便于维护

  • En gine源码准备过程过于复杂,需要对git库重置和切换分支

  • 另外Flu tter Engine从1.5.4升级到1.9.1,Flutter Engine的产物结构发生了变化。

因此,我们 对Engine打包发布的脚本进行了重写,简化编译发布的流程。

背景知识

想要对Engine进行定制,首先就要熟悉它的编译和调试,虽然Flutter官方文档中对Engine的编译有说明,但内容比较分散,很多地方讲解得也不够详细。

通过依赖关系确定代码版本

在我们使用Flutter开发的时候最直接接触的并不是Flutter Engine 而是 Flutter Framework。 所以我们第一步就是要安装我们需要使用的Flutter Framework的版本,比如我们需要使用Flutter 1.9.1 ,则本地拉取对应tag的Flutter 进行安装,从Flutter Framework目录下的bin/internal/engine.version文件中我们可以看到对应的Flutter Engine的版本 ,这个版本是通过Flutter Engine对应commit id(git提交的sha-1哈希值)来表示的。

我们可以先把Flutter Engine的代码clone下来看下,clone之后 checkout到上面的commit节点,Flutter Engine根目录下面有一个比较重要的文件DEPS , 这个文件中描述了所有的依赖,如果你需要对其中的某些依赖比如skia,boringssl做定制的话,那么就需要基于这里面声明的版本来进行相应的修改。

工具链

在编译之前我们还需要了解下Flutter Engine编译所使用的一些工具

  • gclient,https://www.chromium.org/developers/how-tos/depottools/gclient ,这是chromium所使用的一个源码库管理的工具,可以很好的管理源码以及对应的依赖,通过gclinet我们可以获取所有的编译需要的源码和依赖

  • ninja,https://ninja-build.org/  ,编译工具,负责最终的编译工作

  • gn,https://gn.googlesource.com/gn ,负责生成 ninja编译需要的build文件,特别像Flutter这种跨多种操作系统平台跨多种CPU架构的,就需要通过gn生成很多套不同的ninja build文件。

上面的这些工具的使用场景,简单点说就是通过gclient获取Flutter Engine编译所需要的编译环境,源码和依赖库,然后通过gn生成ninja编译所需要的build文件,最终通过ninja来进行编译。

编译

Flutter的编译并不需要我们直接取拉Flutter Engine的源码,都是通过gclient来进行源码和依赖的管理,我们要做的第一步就是创建一个工作目录,比如一个名为engine的目录,目录下创建一个gclient的配置文件.gclient, 此配置文件的语法可以参见 https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/HEAD/README.gclient.md

进入engine目录执行 gclient sync,这个步骤比较耗时,第一次运行,即使100%之后还是会下载东西,我们可以通过进程管理器来查看gclient相应进程(.cipd_client)的网络活动情况,不要提前手动kill掉进程。 第一次gclient sync 执行完成了,engine/src/flutter为Flutter Engine源码的位置,我们需要手动切换到对应的版本分支,然后再次执行gclinet sync对此版本的依赖重新同步下,此次执行会比首次执行快很多。

接下来就是对Engine进行编译了,这里我们以iOS为例,我们编译了iOS模拟器的Flutter Engine的debug产物

gn在生成build文件的时候有不少参数需要我们关注,可以通过类似–ios –android来指定系统平台,不指定则为host平台,比如在macOS中为macOS,在windows中为windows;通过–unoptimized来指定Flutter Engine是否进行debug编译,如果指定了–unoptimized,则打出来的产物会带debug的一些东西,比如额外的log,assert,ios则会带上dSYM信息。所以如果你想要进行Engine源码的调试则必须指定–unoptimized; 另外我们可以通过runtime-mode来指定flutter的运行模式,包含debug,release,profile不指定则为debug。

编译完成后 可以在out对应的目录中看到对应的产物 有两个比较关心的就是 Flutter.framework和clang x64目录下的gen snapshot,其中Flutter.framework是Flutter Engine的编译的结果,gen_snapshot则是担当着dart的编译器。

调试

首先我们可以通过IDE或者flutter命令创建一个demo工程,然后通过命令使用local engine来运行,

flutter run –local-engine-src-path=/Users/Luke/Projects/engine/src  –local-engine=ios
debug sim

unopt

在flutter demo工程下通过local engine的方式运行,这里我们使用的是ios模拟器来进行调试的,运行之后确认模拟器可以正常run起来。这个时候我们通过Xcode打开ios目录下的iOS的工程,会发现Generated.xcconfig中多了一些FLUTTER

ENGINE,LOCAL_ENGINE的内容。

这个时候我们可以在main函数中设置断点(swift的工程没有main的情况下,断点设置在@UIApplicationMain下面)。 debug走到断点的时候我们可以在console中通过br set -f FlutterViewController.mm -l 123来设置断点。

当然还有个更简单的方法,就是将local engine对应的生成的iOS的project拖入demo工程,就可以直接在Engine的源码中设置断点。

这两种方法都可以进行断点调试。

Flutter Engine发布流程定制

上面介绍了如何对官方的engine代码进行编译和调试,但是在真实的开发流程中我们并不能直接使用local engine。

自己的代码库

如果你定制的代码库也是放在github上那么直接fork官方的repo进行修改便可以了,如果代码库需要在自己的服务器上,那么步骤稍微多一些,首先在你自己的git服务中创建自己的repo,比如在自己搭建的gitlab中创建一个MyFlutterEngine的repo,后继就可以进行代码库的准备了。

到这里我们就准备好我们自己的Flutter Engine的代码库了,你可以在里面进行代码的修改。

Flutter Engine 产物发布的格式和方式

但是当我们真正用于线上产品打包发布的时候,我们并不会使用local engine的方式来工作。 Flutter Framework的目录下有一个bin/cache的目录(此目录默认是gitignore的),所有的不同架构不同平台的engine的产物都会缓存在下面,通过检查会发现,这下面的engine产物和我们直接编译得出的产物并不完全一致,所以第一步就需要弄清楚bin/cache下engine产物的结构。

这里我们只关心iOS和安卓,iOS的比较简单就三个目录,ios,ios-profile,ios-release,分别对应debug,profile,release的flutter运行模式,每一个其实都是不同CPU架构进行了合并(通过lipo工具进行合并)主要包含armv7,arm64,这里gen_snapshot有两个版本,分别用于arm64和amrv7的架构进行dart的aot编译,

由于安卓平台中,没法对不同CPU架构进行合并所以安卓产物的目录比较多,

想知道详细的逻辑可以参见flutter tool中关于cache部分的源码 https://github.com/flutter/flutter/blob/v1.9.1-hotfixes/packages/flutter tools/lib/src/cache.dart, 这些Flutter Engine的构建产物在需要的时候从称之为flutter infra的镜像中下载,在国内可以通过国内的镜像(https://storage.flutter-io.cn/flutter infra)进行下载,具体可以查看 https://flutter.dev/community/china 中的说明。

发布流程

经过以上的了解,我们可以开始着手准备Flutter Engine定制化的发布了。

我们可以通过一个git库来管理我们的发布脚本和一些配置文件,这样可以保证别人只要clone下此库就可以直接使用了。

如果需要支持多Flutter Engine版本的打包发布,可以一个版本对应一个打包发布脚本,将公用的方法比如log,打包状态这些抽离到公用的脚本中。

以下为单个Flutter Engine版本的发布的流程:

首先我们需要准备一个.gclient文件,实际使用时候可以将此文件做成一个模版文件,每一个Flutter Engine版本对应一个.gclient模版文件,在gclient sync之前将相应版本的模版拷贝成.gclient。

在.gclient的配置中我们可以直接指定好Flutter Engine代码及其对应的revision,如果部分依赖的库也需要修改,则可以在custom_deps中加入需要修改的依赖库的git地址及其revision,指定好revision可以避免首次gclient sync之后需要额外切换Flutter Engine的代码分支后再gclient sync的情况,也不需要手动去修改定制过的依赖的代码库和分支,可以减少不少工作量。

v1.9.1版本的.gclient模版文件:

gclient sync将Flutter Engine代码以及对应的依赖都准备好了之后就是编译的工作了,同步完成后目录下面出现的src目录其本身也是一个git库,具体可查看 https://github.com/flutter/buildroot ,内容主要是Flutter Engine的编译环境,src下面的flutter则为Flutter Engine的代码,下面是具体的编译脚本,这里以iOS为例

执行之后所有的初步的产物都会在out对应的子目录中,现在我们再次进入到Flutter Engine 编译的根目录中,进行产物的组装和发布,在这里我们目前采用了一种比较简单的发布方案,我们将自己的Flutter Framework的bin/cache目录从gitignore中移除,发布的时候就将产物覆盖然后提交到我们自己的Flutter Framework的库中,缺点就是Flutter Framework的git库体积会比较大,而且后继万一官方做一些缓存策略的改变也会被影响到。

下面以iOS debug的产物为例,release,profile都是类似的过程:

收益

  • 通过将此脚本,只要我们下载好发布工具库,直接执行脚本就可以自动开始编译发布了,真正做到开箱即用,免去别的配置和准备。

  • .gclient 使用模版文件,不同版本的engine对应不同的模版,打包时拷贝执行

  • .gclient 中指定好版本分支和自定义的依赖信息,源代码和依赖sync后一步到位,避免二次切换分支

  • 打包流程脚本和公用方法分离,不同版本的打包脚本独立分开,通用方法共享,方便维护

后续计划

前面提到将产物放到Flutter Framework的bin/cache目录下并不是最优的方案,通过官方的文档可以知道通过设置FLUTTER STORAGE BASE URL的环境变量可以更改flutter infra的镜像的地址,所以后继的方案就是搭建自己的flutter infra镜像,产物编译完成后提交到自己的镜像网站中。

闲鱼团队是Flutter+Dart FaaS前后端一体化新技术的行业领军者,就是现在! 客户端/服务端java/架构/前端/质量工程师 面向社会招聘,base杭州阿里巴巴西溪园区,一起做有创想空间的社区产品、做深度顶级的开源项目,一起拓展技术边界成就极致!

*投喂简历给小闲鱼→ guicai.gxy@alibaba-inc.com

开源项目、峰会直击、关键洞察、深度解读

请认准 闲鱼技术