深入理解flutter的编译原理与优化

Posted 闲鱼技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解flutter的编译原理与优化相关的知识,希望对你有一定的参考价值。

问题背景

对于开发者而言,什么是Flutter?它是用什么语言编写的,包含哪几部分,是如何被编译,运行到设备上的呢?Flutter如何做到Debug模式Hot Reload快速生效变更,Release模式原生体验的呢?Flutter工程和我们的android/ios工程有何差别,关系如何,又是如何嵌入Android/iOS的呢?Flutter的渲染和事件传递机制如何工作?Flutter支持热更新吗?Flutter官方并未提供iOS下的armv7支持,确实如此吗?在使用Flutter的时候,如果发现了engine的bug,如何去修改和生效?构建缓慢或出错又如何去定位,修改和生效呢?

凡此种种,都需要对Flutter从设计,开发构建,到最终运行有一个全局视角的观察。

本文将以一个简单的hello_flutter为例,介绍下Flutter相关原理及定制与优化。

Flutter简介

Flutter的架构主要分成三层:Framework,Engine和Embedder。

Framework使用dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。此部分的核心代码是:flutter仓库下的flutter package,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。

Engine使用C++实现,主要包括:Skia,Dart和Text。Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。其已作为Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS等其他众多产品的图形引擎,支持平台还包括Windows7+,macOS 10.10.5+,iOS8+,Android4.1+,Ubuntu14.04+等。Dart部分主要包括:Dart Runtime,Garbage Collection(GC),如果是Debug模式的话,还包括JIT(Just In Time)支持。Release和Profile模式下,是AOT(Ahead Of Time)编译成了原生的arm代码,并不存在JIT部分。Text即文本渲染,其渲染层次如下:衍生自minikin的libtxt库(用于字体选择,分隔行);HartBuzz用于字形选择和成型;Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体。

深入理解flutter的编译原理与优化

Embedder是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

Flutter工程结构

本文使用开发环境为flutter beta v0.3.1,对应的engine commit:09d05a389。

以hello_flutter工程为例,Flutter工程结构如下所示:

深入理解flutter的编译原理与优化

其中ios为iOS部分代码,使用CocoaPods管理依赖,android为Android部分代码,使用Gradle管理依赖,lib为dart代码,使用pub管理依赖。类似iOS中Cocoapods的Podfile和Podfile.lock,pub下对应的是pubspec.yaml和pubspec.lock。

Flutter模式

对于Flutter,它支持常见的debug,release,profile等模式,但它又有其不一样。

Debug模式:对应了Dart的JIT模式,又称检查模式或者慢速模式。支持设备,模拟器(iOS/Android),此模式下打开了断言,包括所有的调试信息,服务扩展和Observatory等调试辅助。此模式为快速开发和运行做了优化,但并未对执行速度,包大小和部署做优化。Debug模式下,编译使用JIT技术,支持广受欢迎的亚秒级有状态的hot reload。

Release模式:对应了Dart的AOT模式,此模式目标即为部署到终端用户。只支持真机,不包括模拟器。关闭了所有断言,尽可能多地去掉了调试信息,关闭了所有调试工具。为快速启动,快速执行,包大小做了优化。禁止了所有调试辅助手段,服务扩展。

Profile模式:类似Release模式,只是多了对于Profile模式的服务扩展的支持,支持跟踪,以及最小化使用跟踪信息需要的依赖,例如,observatory可以连接上进程。Profile并不支持模拟器的原因在于,模拟器上的诊断并不代表真实的性能。

鉴于Profile同Release在编译原理等上无差异,本文只讨论Debug和Release模式。

事实上flutter下的iOS/Android工程本质上依然是一个标准的iOS/Android的工程,flutter只是通过在BuildPhase中添加shell来生成和嵌入App.framework和Flutter.framework(iOS),通过gradle来添加flutter.jar和vm/isolatesnapshotdata/instr(Android)来将Flutter相关代码编译和嵌入原生App而已。因此本文主要讨论因flutter引入的构建,运行等原理。编译target虽然包括arm,x64,x86,arm64,但因原理类似,本文只讨论arm相关(如无特殊说明,android默认为armv7)。

Flutter代码的编译与运行(iOS)

Release模式下的编译

Release模式下,flutter下iOS工程dart代码构建链路如下所示:

深入理解flutter的编译原理与优化

其中gen_snapshot是dart编译器,采用了tree shaking(类似依赖树逻辑,可生成最小包,也因而在Flutter中禁止了dart支持的反射特性)等技术,负责生成汇编形式机器代码。再通过xcrun等工具链生成最终的App.framework。所有的dart代码,包括业务代码,三方package代码,它们所依赖的flutter框架代码,最终将会编译成App.framework。

PS.tree shaking功能位于gensnapshot中,对应逻辑参见: engine/src/thirdparty/dart/runtime/vm/compiler/aot/precompiler.cc

dart代码最终对应到App.framework中的符号如下所示:

深入理解flutter的编译原理与优化

事实上,类似Android Release下的产物(见下文),App.framework也包含了kDartVmSnapshotData,kDartVmSnapshotInstructions,kDartIsolateSnapshotData,kDartIsolateSnapshotInstructions四个部分。为什么iOS使用App.framework这种方式,而不是Android的四个文件的方式呢?原因在于在iOS下,因为系统的限制,Flutter引擎不能够在运行时将某内存页标记为可执行,而Android是可以的。

Flutter.framework对应了Flutter架构中的engine部分,以及Embedder。实际中Flutter.framework位于flutter仓库的/bin/cache/artifacts/engine/ios*下,默认从google仓库拉取。当需要自定义修改的时候,可通过下载engine源码,利用Ninja构建系统来生成。

Flutter相关代码的最终产物是:App.framework(dart代码生成)和Flutter.framework(引擎)。从Xcode工程的视角看,Generated.xcconfig描述了Flutter相关环境的配置信息,然后Runner工程设置中的Build Phases新增的xcode_backend.sh实现了Flutter.framework的拷贝(从Flutter仓库的引擎到Runner工程根目录下的Flutter目录)与嵌入,App.framework的编译与嵌入。最终生成的Runner.app中Flutter相关内容如下所示:

深入理解flutter的编译原理与优化

其中flutter_assets是相关的资源,代码则是位于Frameworks下的App.framework和Flutter.framework。

Release模式下的运行

Flutter相关的渲染,事件,通信处理逻辑如下所示:

深入理解flutter的编译原理与优化

其中dart中的main函数调用栈如下:

深入理解flutter的编译原理与优化

Debug模式下的编译

Debug模式下flutter的编译,结构类似Release模式,差异主要表现为两点:

1.Flutter.framework

因为是Debug,此模式下Framework中是有JIT支持的,而在Release模式下并没有JIT部分。

2.App.framework

不同于AOT模式下的App.framework是Dart代码对应的机器代码,JIT模式下,App.framework只有几个简单的API,其Dart代码存在于snapshot_blob.bin文件里。这部分的snapshot是脚本快照,里面是简单的标记化的源代码。所有的注释,空白字符都被移除,常量也被规范化,没有机器码,tree shaking或混淆。

App.framework中的符号表如下所示:

深入理解flutter的编译原理与优化

对Runner.app/flutterassets/snapshotblob.bin执行strings命令可以看到如下内容:

深入理解flutter的编译原理与优化

Debug模式下main入口的调用堆栈如下:

深入理解flutter的编译原理与优化

Flutter代码的编译与运行(Android)

鉴于Android和iOS除了部分平台相关的特性外,其他逻辑如Release对应AOT,Debug对应JIT等均类似,此处只涉及两者不同。

Release模式下的编译

release模式下,flutter下Android工程中dart代码整个构建链路如下所示:

深入理解flutter的编译原理与优化

其中vm/isolatesnapshotdata/instr内容均为arm指令,其中vm中涉及runtime等服务(如gc),用于初始化DartVM,调用入口见DartInitialize(dartapi.h)。isolate则对应了我们的应用dart代码,用于创建一个新的isolate,调用入口见DartCreateIsolate(dart_api.h)。flutter.jar类似iOS的Flutter.framework,包括了Engine部分(Flutter.jar中的libflutter.so),和Embedder部分(FlutterMain,FlutterView,FlutterNativeView等)。实际中flutter.jar位于flutter仓库的/bin/cache/artifacts/engine/android*下,默认从google仓库拉取。需要自定义修改的时候,可通过下载engine源码,利用Ninja构建系统来生成flutter.jar。

以isolatesnapshotdata/instr为例,执行disarm命令结果如下:

深入理解flutter的编译原理与优化

深入理解flutter的编译原理与优化)

其Apk结构如下所示:

深入理解flutter的编译原理与优化

APK新安装之后,会根据一个判断逻辑(packageinfo中的versionCode结合lastUpdateTime)来决定是否拷贝APK中的assets,拷贝后内容如下所示:

深入理解flutter的编译原理与优化

isolate/vmsnapshotdata/instr均最后位于app的本地data目录下,而此部分又属于可写内容,可通过下载并替换的方式,完成App的动态更新。

Release模式下的运行

深入理解flutter的编译原理与优化

Debug模式下的编译

类似iOS的Debug/Release的差别,Android的Debug与Release的差异主要包括以下两部分:

1.flutter.jar

区别同iOS

2.App代码部分

位于flutterassets下的snapshotblob.bin,同iOS。

在介绍了iOS/Android下的Flutter编译原理后,下面介绍下如何定制flutter/engine以完成定制和优化。鉴于Flutter处于敏捷的迭代中,现有的问题后续不一定是问题,因而此部分并不是要解决多少问题,而是说明不同问题下的解决思路。

Flutter构建相关的定制与优化

Flutter是一个很复杂的系统,除了上述提到的三层架构中的内容外,还包括Flutter Android Studio(Intellij)插件,pub仓库管理等。但我们的定制和优化往往是flutter的工具链相关逻辑,其逻辑位于flutter仓库的flutter_tools包。下面举例说明下如何针对此部分做定制。

Android部分

相关内容包括flutter.jar,libflutter.so(位于flutter.jar下),gensnapshot,flutter.gradle,flutter(fluttertools)。

1.限定Android中target为armeabi

此部分属于构建相关,逻辑位于flutter.gradle下。当App是通过armeabi支持armv7/arm64的时候,需要修改flutter的默认逻辑。如下所示:

深入理解flutter的编译原理与优化

因为gradle本身的特点,此部分修改后直接构建即可生效。

2.设定Android启动时默认使用第一个launchable-activity

此部分属于flutter_tools相关,修改如下:

深入理解flutter的编译原理与优化

这里的重点不是如何去修改,而是如何去让修改生效。原理上,flutter run/build/analyze/test/upgrade等命令实际上执行的都是flutter(flutter/bin/flutter)这一脚本,再透过dart执行fluttertools.snapshot(通过packages/fluttertools生成),逻辑如下:

 
   
   
 
  1. if [[ ! -f "SNAPSHOT_PATH" ]] || [[ ! -s "STAMP_PATH" ]] || [[ "(cat "STAMP_PATH")" != "revision" ]] || [[ "FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then

  2.        rm -f "$FLUTTER_ROOT/version"

  3.        touch "$FLUTTER_ROOT/bin/cache/.dartignore"

  4.        "$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"

  5.        echo Building flutter tool...

  6.    if [[ "$TRAVIS" == "true" ]] || [[ "$BOT" == "true" ]] || [[ "$CONTINUOUS_INTEGRATION" == "true" ]] || [[ "$CHROME_HEADLESS" == "1" ]] || [[ "$APPVEYOR" == "true" ]] || [[ "$CI" == "true" ]]; then

  7.      PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_bot"

  8.    fi

  9.    export PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_install"

  10.    if [[ -d "$FLUTTER_ROOT/.pub-cache" ]]; then

  11.      export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_ROOT/.pub-cache"}"

  12.    fi

  13.    while : ; do

  14.      cd "$FLUTTER_TOOLS_DIR"

  15.      "$PUB" upgrade --verbosity=error --no-packages-dir && break

  16.      echo Error: Unable to 'pub upgrade' flutter tool. Retrying in five seconds...

  17.      sleep 5

  18.    done

  19.    "$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH"

  20.    echo "$revision" > "$STAMP_PATH"

  21.    fi

不难看出要重新构建fluttertools,可以删除flutterrepodir/bin/cache/fluttertools.stamp(这样重新生成一次),或者屏蔽掉if/fi判断(每一次都会重新生成)。

3.如何在Android工程Debug模式下使用release模式的flutter

研发中如果发现flutter有些卡顿,可能是逻辑的原因,也可能是是Debug模式。此时可以构建release下的apk,也可以将flutter强制修改为release模式如下:

深入理解flutter的编译原理与优化

iOS部分

相关内容包括:Flutter.framework,gensnapshot,xcodebackend.sh,flutter(flutter_tools)。

1.优化构建过程中反复替换Flutter.framework导致的重新编译

此部分逻辑属于构建相关,位于xcode_backend.sh中,Flutter为了保证获取到正确的Flutter.framework,每次都会基于配置(见Generated.xcconfig配置)查找和替换Flutter.framework,这也导致工程中对此Framework有依赖代码的重新编译,修改如下:

深入理解flutter的编译原理与优化

2.如何在iOS工程Debug模式下使用release模式的flutter

将Generated.xcconfig中的FLUTTERBUILDMODE修改为release,FLUTTERFRAMEWORKDIR修改为release对应的路径即可。

3.armv7的支持

原始文章请参见:https://github.com/flutter/engine/wiki/iOS-Builds-Supporting-ARMv7

事实上flutter本身是支持iOS下的armv7的,但v0.3.1下并未提供官方支持,需自行修改相关逻辑,具体如下:

a.默认的逻辑可以生成Flutter.framework(arm64)

b.修改flutter以使得fluttertools可以每次重新构建,修改buildaot.dart和mac.dart,将针对iOS的arm64修改为armv7,修改gen_snapshot为i386架构。

其中i386架构下的gen_snapshot可通过以下命令生成:

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

  2. ninja -C out/ios_release_arm

这里有一个隐含逻辑:

构建gensnapshot的CPU相关预定义宏(x8664/i386等),目标gensnapshot的arch,最终的App.framework的架构整体上要保持一致。即x8664->x86_64->arm64或者i386->i386->armv7。

c.在iPhone4S上,会发生因gensnapshot生成不被支持的SDIV指令而造成EXCBADINSTRUCTION(EXCARMUNDEFINED)错误,可通过给gensnapshot添加参数--no-use-integer-division实现(位于build_aot.dart)。其背后的逻辑(dart编译arm代码逻辑流)如下图所示:

深入理解flutter的编译原理与优化

d.基于a和b生成的Flutter.framework,将其lipo create生成同时支持armv7和arm64的Flutter.framework。

e.修改Flutter.framework下的Info.plist,移除

 
   
   
 
  1.  <key>UIRequiredDeviceCapabilities</key>

  2.  <array>

  3.    <string>arm64</string>

  4.  </array>

同理,对于App.framework也要作此操作,以免上架后会受到App Thining的影响。

flutter_tools的调试

如果想了解flutter如何构建debug模式下apk时,具体执行的逻辑如何,可以参考下面的思路:

a.了解flutter_tools的命令行参数

深入理解flutter的编译原理与优化

b.以dart工程形式打开packages/fluttertools,基于获得的参数修改fluttertools.dart,设置命令行dart app即可开始调试。

定制engine与调试

假设我们在flutter beta v0.3.1的基础上进行定制与业务开发,为了保证稳定,一定周期内并不升级SDK,而此时,flutter在master上修改了某个v0.3.1上就有的bug,记为fixbugcommit。如何才能跟踪和管理这种情形呢?

1.flutter beta v0.3.1指定了其对应的engine commit为:09d05a389,见flutter/bin/internal/engine.version。

2.获取engine代码

3.因为2中拿到的是master代码,而我们需要的是特定commit(09d05a389)对应的代码库,因而从此commit拉出新分支:custombetav0.3.1。

4.基于custombetav0.3.1(commit:09d05a389),执行gclient sync,即可拿到对应flutter beta v0.3.1的所有engine代码。

5.使用git cherry-pick fixbugcommit将master的修改同步到custombetav0.3.1,如果修改有很多对最新修改的依赖,可能会导致编译失败。

6.对于iOS相关的修改执行以下代码:

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

  2. ninja -C out/ios_debug_arm

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

  4. ninja -C out/ios_release_arm

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

  6. ninja -C out/ios_profile_arm

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

  8. ninja -C out/ios_debug

  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=arm64

  12. ninja -C out/ios_profile

即可生成针对iOS的arm/arm64&debug/release/profile的产物。可用构建产物替换flutter/bin/cache/artifacts/engine/ios*下的Flutter.framework和gen_snapshot。

如果需要调试Flutter.framework源代码,构建的时候命令如下:

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

  2. ninja -C out/ios_debug_unopt

用生成产物替换掉flutter中的Flutter.framework和gen_snapshot,即可调试engine源代码。

7.对于Android相关的修改执行以下代码:

 
   
   
 
  1. ./flutter/tools/gn --runtime-mode=debug --android --android-cpu=arm

  2. ninja -C out/android_debug

  3. ./flutter/tools/gn --runtime-mode=release --android --android-cpu=arm

  4. ninja -C out/android_release

  5. ./flutter/tools/gn --runtime-mode=profile --android --android-cpu=arm

  6. ninja -C out/android_profile

即可生成针对Android的arm&debug/release/profile的产物。可用构建产物替换flutter/bin/cache/artifacts/engine/android*下的gen_snapshot和flutter.jar。

后续主题

后续我们将就以下主题继续分享:

a.Flutter架构中Embedder如何处理渲染和事件(点击等)传递,如何管理线程和消息循环,Channel如何工作。

b.Engine中Dart的编译调试如何工作,Skia内部又是如何处理渲染的。

c.Native工程如何使用Flutter实现渐进式的重构与迁移。

d.如何搭建私有仓库,实现pub对于多仓库的支持

...

联系我们

如果对文本的内容有疑问或指正,欢迎告知我们。

另闲鱼技术团队诚聘各路英才,flutter,C++,iOS/Android,Java都要,欢迎发消息给我们。

参考文档

1.Flutter's modes

2.iOS Builds Supporting ARMv7

3.Contributing to the Flutter engine

4.Flutter System Architecture

5.The magic of flutter

6.Symbolicating production crash stacks

7.flutter.io

8.获取本文使用的源代码


以上是关于深入理解flutter的编译原理与优化的主要内容,如果未能解决你的问题,请参考以下文章

深入理解PHP原理之Opcodes

深入理解 Java 虚拟机之学习笔记

《深入理解 Java 虚拟机》读书笔记:晚期(运行期)优化

深入理解Java的动态编译

深入理解Java的动态编译

掌握查询利器 深入理解PostgreSQL索引原理与优化