问题背景

对于开发者而言,什么是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来渲染字体。

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

Flutter工程结构

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

以hello_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代码构建链路如下所示:

其中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中的符号如下所示:

事实上,类似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_assets是相关的资源,代码则是位于Frameworks下的App.framework和Flutter.framework。

Release模式下的运行

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

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

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中的符号表如下所示:

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

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

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

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

Release模式下的编译

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

其中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命令结果如下:

)

其Apk结构如下所示:

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

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

Release模式下的运行

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的默认逻辑。如下所示:

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

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

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

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

if [[ ! -f "SNAPSHOT_PATH" ]] || [[ ! -s "STAMP_PATH" ]] || [[ "(cat "STAMP_PATH")" != "revision" ]] || [[ "FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then    rm -f "$FLUTTER_ROOT/version"   touch "$FLUTTER_ROOT/bin/cache/.dartignore" "$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh" echo Building flutter tool...   if [[ "$TRAVIS" == "true" ]] || [[ "$BOT" == "true" ]] || [[ "$CONTINUOUS_INTEGRATION" == "true" ]] || [[ "$CHROME_HEADLESS" == "1" ]] || [[ "$APPVEYOR" == "true" ]] || [[ "$CI" == "true" ]]; then    PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_bot"  fi  export PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_install"   if [[ -d "$FLUTTER_ROOT/.pub-cache" ]]; then    export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_ROOT/.pub-cache"}" fi  while : ; do    cd "$FLUTTER_TOOLS_DIR" "$PUB" upgrade --verbosity=error --no-packages-dir && break echo Error: Unable to 'pub upgrade' flutter tool. Retrying in five seconds...   sleep 5 done    "$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH"    echo "$revision" > "$STAMP_PATH" fi

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

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

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

iOS部分

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

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

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

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可通过以下命令生成:

./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm
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代码逻辑流)如下图所示:

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

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

  <key>UIRequiredDeviceCapabilities</key> <array>   <string>arm64</string>  </array>

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

flutter_tools的调试

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

a.了解flutter_tools的命令行参数

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相关的修改执行以下代码:

./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm
ninja -C out/ios_debug_arm  ./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm
ninja -C out/ios_release_arm    ./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm
ninja -C out/ios_profile_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=arm64
ninja -C out/ios_release    ./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm64
ninja -C out/ios_profile

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

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

./flutter/tools/gn --runtime-mode=debug --unoptimized --ios --ios-cpu=arm64
ninja -C out/ios_debug_unopt

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

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

./flutter/tools/gn --runtime-mode=debug --android --android-cpu=arm
ninja -C out/android_debug  ./flutter/tools/gn --runtime-mode=release --android --android-cpu=arm
ninja -C out/android_release    ./flutter/tools/gn --runtime-mode=profile --android --android-cpu=arm
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的编译原理与优化相关推荐

  1. 深入理解 Flutter 的编译原理与优化

    阿里妹导读:对于开发者而言,Flutter工程和我们的Android/iOS工程有何差别?Flutter的渲染和事件传递机制如何工作?构建缓慢或出错又如何去定位,修改和生效呢?凡此种种,都需要对Flu ...

  2. 【Java核心技术大会 PPT分享】陈阳:深入理解 Java 虚拟机编译原理

    导读:深入理解 Java 虚拟机编译原理 直播分享PPT Java核心技术大会2022 分享主题:深入理解 Java 虚拟机编译原理 分享嘉宾:陈阳,京东科技架构师,曾就职于美团.去哪网,负责自研消息 ...

  3. 理解前端Babel编译原理

    大厂技术  坚持周更  精选好文 背景 我们知道编程语言主要分为「编译型语言」和「解释型语言」,编译型语言是在代码运行前编译器将编程语言转换成机器语言,运行时不需要重新翻译,直接使用编译的结果就行了. ...

  4. 深入理解include预编译原理

    http://ticktick.blog.51cto.com/823160/596179 你了解 #include 某个 .h 文件后,编译器做了哪些操作么? 你清楚为什么在 .h文件中定义函数实现的 ...

  5. 编译原理词法分析器的c++实现

    一.题目的理解和说明 编译原理这门课是计算机专业的核心课程之一,是一门研究软件是什么,为什么可以运行,以及怎么运行的学科.编译系统的改进将会直接对其上层的应用程序的执行效率,执行原理产生深刻的影响.编 ...

  6. 龙书啃不动?老司机带你从零入门编译原理,开发编译器

    计算机只认识二进制的,但是我们平常开发中根本不会使用二进制进行开发,我们使用的都是 Java.C.Python 这类的高级语言.每种语言都会经过一系列的转换才能被计算机识别,那么到底是谁做的这项工作呢 ...

  7. JVM成神之路-HotSpot虚拟机-编译原理、JIT、编译优化

    Java编译原理 什么是字节码.机器码.本地代码? 字节码是指平常所了解的 .class 文件,Java 代码通过 javac 命令编译成字节码 机器码和本地代码都是指机器可以直接识别运行的代码,也就 ...

  8. 深入理解计算机原理与编译原理,【底层原理:深入理解计算机系统】#1 一切从'hello world'说起 (一)...

    计算机系统是由硬件和系统软件组成的,他们共同工作来运行应用程序.虽然系统的具体实现方式随着时间不断的在变化,但是系统的内在概念却没有改变的. 所有的计算机硬件和软件有着相似的结构和功能.这个系列专题便 ...

  9. 前端面试 vue生命周期钩子是如何实现的?理解vue中模板编译原理?

    生命周期钩子在内部会被vue维护成一个数组(vue 内部有一个方法mergeOption)和全局的生命周期合并最终转换成数组,当执行到具体流程时会执行钩子(发布订阅模式),callHook来实现调用. ...

最新文章

  1. 按下开机键,计算机背后的故事
  2. python中选择结构通过什么语句实现_Python中选择结构通过什么语句实现
  3. cad lisp 两侧偏移并删除_弱电CAD不算CAD?学学操作,将手速发挥极致,让他人刮目相看...
  4. 聊聊网易技术如何帮教育行业开出花
  5. Windows 7安装PlayReady出现“任务被禁用”错误信息
  6. Android项目运行junit测试类时出现错误Internal Error (classFileParser.cpp:3494)的解决办法...
  7. 不止操作系统,智能手机才更需要开源!
  8. 【BZOJ4025】二分图(可撤销并查集+线段树分治)
  9. 如何理解CPU上下文切换(二)
  10. Ubuntu如何修改用户密码
  11. 安装英文版xp时选择安装亚洲中文语言包
  12. 手把手教你使用《ProxyMan》抓取App接口
  13. API的理解和使用——全局命令
  14. 服务器系统排行榜,服务器操作系统排行榜
  15. 解决U盘中文件全部变成快捷方式的问题
  16. Qt的QBuffer
  17. 正太分布函数和反函数 标量值函数 (借鉴)
  18. H5游戏开发:决胜三分球
  19. Linux下缓冲区溢出攻击的原理及对策
  20. 嵌入式C语言自我修养:从芯片、编译器到操作系统-习题、笔记

热门文章

  1. 【陈工笔记】# PyTorch 在win10系统下的环境配置 (Pycharm)
  2. 正负样本不均衡的解决办法
  3. Windows 7下通过anaconda安装tensorflow
  4. CentOS7的Tiger VNC设置
  5. kafka record(s) for xxxxxx: 30043 ms has passed since last append异常
  6. 高仿钉钉和小米的日历控件
  7. 快应用:足以超越原生APP
  8. 一个有趣的TimesTen大数据案例-美国邮政
  9. sklearn波士顿房价数据集——线性回归
  10. 2021年全球复合半导体收入大约1083.1百万美元,预计2028年达到1580.8百万美元