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

引言

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

  • 没法做到开箱即用

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

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

  • 另外Flutter 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


     
     
     
     
  1. solutions = [

  2. {

  3. "managed": False,

  4. "name": "src/flutter",

  5. "url": "https://github.com/flutter/flutter.git",

  6. "custom_deps": {},

  7. "deps_file": "DEPS",

  8. "safesync_url": "",

  9. },

  10. ]

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

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


     
     
     
     
  1. ./flutter/tools/gn --ios --simulator --unoptimized #生成ninja编译的配置文件

  2. ./flutter/tools/gn --unoptimized

  3. 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信息。所以如果你想要进行Engine源码的调试则必须指定--unoptimized; 另外我们可以通过runtime-mode来指定flutter的运行模式,包含debug,release,profile不指定则为debug。

编译完成后 可以在out对应的目录中看到对应的产物 有两个比较关心的就是 Flutter.framework和clangx64目录下的gensnapshot,其中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=iosdebugsimunopt
在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,后继就可以进行代码库的准备了。


     
     
     
     
  1. git clone [email protected]:xxxx/MyFlutterEngine.git

  2. git remote add upstream https://github.com/flutter/engine.git

  3. git fetch upstream

  4. git checkout upstream/v1.9.1-hotfixes

  5. git branch v1.9.1

  6. git checkout v1.9.1

  7. git push origin v1.9.1

到这里我们就准备好我们自己的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编译,
怎样的Flutter Engine定制流程,才能实现真正“开箱即用”?_第1张图片

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

怎样的Flutter Engine定制流程,才能实现真正“开箱即用”?_第2张图片

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

发布流程

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

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

怎样的Flutter Engine定制流程,才能实现真正“开箱即用”?_第3张图片

首先我们需要准备一个.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模版文件:


     
     
     
     
  1. solutions = [

  2. {

  3. "managed": False,

  4. "name": "src/flutter",

  5. "url": "[email protected]:xxxx/[email protected]",

  6. "custom_deps": {

  7. "src/third_party/skia":"[email protected]:xxxx/[email protected]",

  8. "src/third_party/boringssl/src":"[email protected]:xxxx/[email protected]",

  9. },

  10. "deps_file": "DEPS",

  11. "safesync_url": "",

  12. },

  13. ]

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


     
     
     
     
  1. ./flutter/tools/gn --runtime-mode=debug --ios --simulator

  2. ninja -C out/ios_debug_sim

  3. ./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm

  4. ninja -C out/ios_debug_arm

  5. ./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm64

  6. ninja -C out/ios_debug

  7. ./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm

  8. ninja -C out/ios_release_arm

  9. ./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm64

  10. ninja -C out/ios_release

  11. ./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm

  12. ninja -C out/ios_profile_arm

  13. ./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm64

  14. ninja -C out/ios_profile

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

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


     
     
     
     
  1. # ios debug

  2. cp -rf src/out/ios_debug/Flutter.framework tmp/

  3. lipo -create -output tmp/Flutter.framework/Flutter \

  4. src/out/ios_debug/Flutter.framework/Flutter \

  5. src/out/ios_debug_arm/Flutter.framework/Flutter \

  6. src/out/ios_debug_sim/Flutter.framework/Flutter

  7. cd tmp

  8. zip -r Flutter.framework.zip Flutter.framework

  9. cd ..

  10. mkdir -p "${flutter_path}"/bin/cache/artifacts/engine/ios

  11. cp -rf tmp/Flutter.framework "${flutter_path}"/bin/cache/artifacts/engine/ios/

  12. cp -f tmp/Flutter.framework.zip "${flutter_path}"/bin/cache/artifacts/engine/ios/

  13. cp -f src/out/ios_debug/clang_x64/gen_snapshot "${flutter_path}"/bin/cache/artifacts/engine/ios/gen_snapshot_arm64

  14. cp -f src/out/ios_debug_arm/clang_x64/gen_snapshot "${flutter_path}"/bin/cache/artifacts/engine/ios/gen_snapshot_armv7

  15. rm -rf tmp/*

收益

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

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

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

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

后续计划

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

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

*投喂简历给小闲鱼→[email protected]

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

请认准闲鱼技术

你可能感兴趣的:(怎样的Flutter Engine定制流程,才能实现真正“开箱即用”?)