天天看点

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

作者:闲鱼技术-卢克

引言

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

没法做到开箱即用

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

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

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

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

1.背景知识

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

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

在我们使用Flutter开发的时候最直接接触的并不是Flutter Engine 而是 Flutter Framework(

https://github.com/flutter/flutter)

。所以我们第一步就是要安装我们需要使用的Flutter Framework的版本,比如我们需要使用Flutter 1.9.1 ,则本地拉取对应tag的Flutter 进行安装 

https://github.com/flutter/flutter/releases/tag/v1.9.1%2Bhotfix.6

,从Flutter Framework目录下的bin/internal/engine.version文件中我们可以看到对应的Flutter Engine的版本 b863200c37df4ed378042de11c4e9ff34e4e58c9 ,这个版本是通过Flutter Engine对应commit id(git提交的sha-1哈希值)来表示的。

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

1.2.工具链

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

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

1.3.编译

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

https://chromium.googlesource.com/chromium/tools/depot_tools.git/+/HEAD/README.gclient.md
solutions = [
  {
    "managed": False,
    "name": "src/flutter",
    "url": "https://github.com/flutter/flutter.git",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]           

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

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

./flutter/tools/gn --ios --simulator --unoptimized  #生成ninja编译的配置文件 
./flutter/tools/gn --unoptimized
ninja -C out/ios_debug_sim_unopt && ninja -C out/host_debug_unopt           

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

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

同时还生成了iOS的工程文件,我们可以通过Xcode打开查看所有的Flutter Engine的源码

1.4.调试

首先我们可以通过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的内容,另外工程中的Flutter.framework已经变成了我们所指定的本地engine的产物(拷贝过来的)。

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

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

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

2. Flutter Engine发布流程定制

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

2.1.自己的代码库

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

git clone [email protected]:xxxx/MyFlutterEngine.git
git remote add upstream https://github.com/flutter/engine.git
git fetch upstream
git checkout upstream/v1.9.1-hotfixes
git branch v1.9.1
git checkout v1.9.1
git push origin v1.9.1           

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

2.2.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编译

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

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

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

想知道详细的逻辑可以参见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

 中的说明。

2.3.发布流程

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

我们可以通过一个git库来管理我们的发布脚本和一些配置文件,这样可以保证别人只要clone下此库就可以直接使用了。如果需要支持多Flutter Engine版本的打包发布,可以一个版本对应一个打包发布脚本,将公用的方法比如log,打包状态这些抽离到公用的脚本中。

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

怎样的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模版文件:

solutions = [
 {
   "managed": False,
   "name": "src/flutter",
   "url": "[email protected]:xxxx/[email protected]",
   "custom_deps": {
   "src/third_party/skia":"[email protected]:xxxx/[email protected]",
   "src/third_party/boringssl/src":"[email protected]:xxxx/[email protected]",
   },
   "deps_file": "DEPS",
   "safesync_url": "",
 },
]           

gclient sync将Flutter Engine代码以及对应的依赖都准备好了之后就是编译的工作了,同步完成后目录下面出现的src目录其本身也是一个git库,具体可查看 

https://github.com/flutter/buildroot

,内容主要是Flutter Engine的编译环境,src下面的flutter则为Flutter Engine的代码,下面是具体的编译脚本,这里以iOS为例

./flutter/tools/gn --runtime-mode=debug --ios --simulator
ninja -C out/ios_debug_sim
./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm
ninja -C out/ios_debug_arm
./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm64
ninja -C out/ios_debug

./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm
ninja -C out/ios_release_arm
./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm64
ninja -C out/ios_release

./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm
ninja -C out/ios_profile_arm
./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm64
ninja -C out/ios_profile           

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

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

# ios debug
cp -rf src/out/ios_debug/Flutter.framework tmp/

lipo -create -output tmp/Flutter.framework/Flutter \
src/out/ios_debug/Flutter.framework/Flutter \
src/out/ios_debug_arm/Flutter.framework/Flutter \
src/out/ios_debug_sim/Flutter.framework/Flutter

cd tmp
zip -r Flutter.framework.zip Flutter.framework
cd ..

mkdir -p "${flutter_path}"/bin/cache/artifacts/engine/ios
cp -rf tmp/Flutter.framework "${flutter_path}"/bin/cache/artifacts/engine/ios/
cp -f tmp/Flutter.framework.zip "${flutter_path}"/bin/cache/artifacts/engine/ios/
cp -f src/out/ios_debug/clang_x64/gen_snapshot "${flutter_path}"/bin/cache/artifacts/engine/ios/gen_snapshot_arm64
cp -f src/out/ios_debug_arm/clang_x64/gen_snapshot "${flutter_path}"/bin/cache/artifacts/engine/ios/gen_snapshot_armv7

rm -rf tmp/*
           

收益

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

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

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

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

后续计划

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