Flutter 多国语言支持,参考 Flutter 2.8.1/Android Studio Bumblebee 2021.1.1多国语支持配置和使用
本地化/国际化应用程序名称 Android
系统与开发环境 macOS Big Sur 11.6.3/Android Studio Bumblebee 2021.1.1
Android下本地化应用程序名比较简单,只需要修改 AndroidManifest.xml 里的 application 标签下的 android:label 即可,如下图:
Flutter 多国语言支持,参考 Flutter 2.8.1/Android Studio Bumblebee 2021.1.1多国语支持配置和使用
系统与开发环境 macOS Big Sur 11.6.3/Android Studio Bumblebee 2021.1.1
Android下本地化应用程序名比较简单,只需要修改 AndroidManifest.xml 里的 application 标签下的 android:label 即可,如下图:
每次我们运行一个新的 Flutter 项目并运行它时,我们都会看到一条消息:
|
1 |
"flutter pub get" in a_flutter_project... |
问题是:当我们获得新的依赖项时会发生什么?
正如我们在上一篇文章中看到的《深入研究 Flutter Pubspec.yaml 文件》我们有不同的方法来向项目添加依赖项,但我们还没有讨论的一件事是为什么我们有时会添加插入^符号在我们的依赖版本之前:
|
1 2 |
# ... flutter_bloc: ^7.0.1 |
这个符号有什么作用?它与pubspec.lock文件有什么关系?
正如Dart官方文档中所见,有几种不同的方式来定义包的版本:
any 或为空 - 这将导致包版本被定义为最新版本或确定相对于其他包约束应该使用哪个版本
<, >, <=, >= - 允许我们选择更低、更高、更低且等于或更高且等于规定版本的版本
但是,如果我们想要有灵活性和一些稳定性的保证,我们应该考虑使用插入^符号。如文档中所定义:
^version表示/保证向后兼容指定版本的所有版本的范围。
因此,遵循语义版本指南(其中1.2.3是Major 1、Minor 2 和Patch 3),这意味着:
对于没有主要版本的依赖项,例如0.12.3和0.0.2-> 插入符号将查找包含在同一次要版本中的依赖项。所以^0.12.3会满足所有版本>= 0.12.3 < 0.13.0。
对于具有主要版本的依赖项,例如1.2.3,插入符号将查找包含在同一主要版本中的依赖项。所以^1.2.3会满足所有版本:>=1.2.3 <2.0.0
理论上(因为在实践中一些库可以打破这个规则),我们不会有从to或 from to 的破坏性更改,这就是为什么 Dart 冒昧地为我们升级这些版本。0.12.30.12.61.2.31.6.90
现在,这引出了两个不同的问题:
要回答这些问题,我们必须了解什么是pubspec.lock文件。
与其尝试从整体上查看文件,不如换一种方式——我们创建一个新项目,提交它,然后添加一个新的依赖项:
|
1 2 3 |
dependencies: #... version_banner: ^0.2.0 |
之后,我们运行flutter pub get并查看pubspec.lock文件的差异内容:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
diff --git a/pubspec.lock b/pubspec.lock index 640db00..7d62228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + package_info: + dependency: transitive + description: + name: package_info + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3+4" path: dependency: transitive description: @@ -149,5 +156,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + version_banner: + dependency: "direct main" + description: + name: version_banner + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" sdks: dart: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" |
我们一点一点来分析一下:
|
1 2 3 4 5 6 7 |
version_banner: dependency: "direct main" description: name: version_banner url: "https://pub.dartlang.org" source: hosted version: "0.2.1" |
version_banner;dependency类型,它告诉我们它是一个direct依赖项,即我们通过pubspec.yaml文件添加的,还是一个transitive依赖项(如package_info锁定文件中的依赖项所示),它是一个依赖项的依赖项;description将相关的依赖性的类型的其他信息(GIT,本地或托管),无论是路径依赖,在git的承诺散列和URL存储库,或托管依赖的URL,在这种情况下, , pub.dev;source告诉我们如何将依赖项添加到项目中。在本例中,我们通过 pub.dev(托管)添加它,但我们也可以使用sourceaspath或git;version将告诉我们我们正在使用的具体版本是什么。如果我们仔细查看version,我们会发现虽然我们在pubspec.yamlversion 中定义了^0.2.0,但从锁定文件中解析出来的版本是0.2.1,这意味着 Dart 能够接受并使用最新版本的库,尊重它的次要版本。
^如果我们只使用一个依赖,使用的好处^只是让我们拥有最新最好的版本。然而,^在我们的依赖项中使用还有另一个很好的理由——它允许 Dart 足够灵活地选择一个与所有瞬态(或依赖项的依赖项)依赖项一致的依赖项版本。
想象一下以下场景 - 您http在应用程序中使用最新版本的包 - http: 0.13.0. 但是,您还使用了另一个名为 library_b 的库,它也依赖于http,但它使用的是更新版本!http: 0.13.3.
|
1 2 3 4 5 6 7 8 |
name: app_a # ... dependencies: library_b: path: ../library_b http: 0.13.0 |
|
1 2 3 4 5 |
name: library_b version: 0.0.1 # ... dependencies: http: 0.13.3 |
那么如果我们选择使用确切的版本会发生什么http: 0.13.3?
|
1 2 3 4 5 6 |
➜ app_a flutter pub get Running "flutter pub get" in app_a... Because app_a depends on library_b from path which depends on http 0.13.3, http 0.13.3 is required. So, because app_a depends on http 0.13.0, version solving failed. pub get failed (1; So, because app_a depends on http 0.13.0, version solving failed.) |
由于我们使用的是硬依赖版本,Dart 将无法找到同时满足这两个约束的版本。但是如果我们添加^到应用程序的依赖项中呢?从理论上讲,这意味着应用程序会说“我可以有一个灵活的 http 版本,所以找到一个既满足又不引起冲突的版本”。
|
1 2 3 4 5 6 7 8 |
name: app_a # ... dependencies: library_b: path: ../library_b http: ^0.13.3 |
就这样,我们运行flutter pub get命令:
|
1 2 |
➜ app_a flutter pub get Running "flutter pub get" in app_a... |
查看pubspec.lock应用程序的文件,我们看到它现在使用更新版本0.13.3:
|
1 2 3 4 5 6 7 |
http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted version: "0.13.3" |
但是,如果版本不同会发生什么?让我们看看http应用程序版本高于库版本的情况:
|
1 2 3 4 5 6 7 8 |
name: app_a # ... dependencies: library_b: path: ../library_b http: ^0.13.3 |
|
1 2 3 4 5 |
name: library_b version: 0.0.1 # ... dependencies: http: 0.13.0 |
该^符号只能找到等于或高于我们声明的版本的版本,这意味着如果我们运行,flutter pub get我们将看到以下错误消息:
|
1 2 3 4 5 6 |
➜ app_a flutter pub get Running "flutter pub get" in app_a... Because app_a depends on library_b from path which depends on http 0.13.0, http 0.13.0 is required. Running "flutter pub get" in app_a... So, because app_a depends on http ^0.13.3, version solving failed. Running "flutter pub get" in app_a... pub get failed (1; So, because app_a depends on http ^0.13.3, version solving failed.) |
解决这个问题的一个唯一的办法就是让这两个库和应用程序依赖于一个^版本,让他们找到一个版本,同时满足。
如果我们无法^在我们正在使用的依赖项中添加表示法,我们总是可以dependency_overrides在我们的pubspec.yaml文件中使用 a ,正如我们在深入研究 Pubspec.yaml 文件中所讨论的那样。
因此,如果通过运行flutter pub get我们没有看到通过pubspec.lock文件对我们的包进行任何更新,那么我们如何更新我们的依赖项?
答案是使用flutter pub upgrade!
让我们以以下应用程序依赖项为例,并假设pubspec.lock文件使用相同的版本:
|
1 2 3 4 5 6 |
name: app_a # ... dependencies: http: ^0.13.0 flutter_bloc: ^7.0.0 |
如果我们运行,flutter pub upgrade我们会看到所有依赖项都将具有更高版本,符合以下标准^:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
➜ app_a git:(master) flutter pub upgrade Resolving dependencies... bloc 7.0.0 characters 1.1.0 collection 1.15.0 ffi 1.1.2 file 6.1.2 flutter 0.0.0 from sdk flutter > flutter_bloc 7.1.0 (was 7.0.0) > shared_preferences 2.0.6 (was 2.0.0) ... Changed 2 dependencies! 1 package has newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information. |
通过查看差异,我们可以看到这些版本已更新到较新的版本:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@@ -47,7 +47,7 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "7.0.0" + version: "7.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -136,7 +136,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.6" shared_preferences_linux: dependency: transitive description: |
但是如果我们不想升级所有的依赖呢?如果我们只想测试特定包的最新版本怎么办?幸运的是,该upgrade命令允许我们使用flutter pub upgrade <package_name>.
让我们通过使用来测试它flutter pub upgrade shared_preferences:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
➜ app_a git:(master) flutter pub upgrade shared_preferences Resolving dependencies... bloc 7.0.0 characters 1.1.0 collection 1.15.0 ffi 1.1.2 file 6.1.2 flutter 0.0.0 from sdk flutter flutter_bloc 7.0.0 (7.1.0 available) > shared_preferences 2.0.6 (was 2.0.0) ... Changed 1 dependency! 2 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information. |
正如我们所看到的,命令行告诉我们,虽然有一个更新的版本可用于flutter_bloc,但它没有被应用。我们可以通过查看差异来验证这一点:
|
1 2 3 4 5 6 7 8 9 |
@@ -136,7 +136,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.6" shared_preferences_linux: dependency: transitive description: |
查看flutter pub upgrade命令的最后一行,我们看到它提到了该flutter pub outdated命令。如果我们希望pubspec.yaml直接更新我们的文件,此命令可用于检查我们软件包的所有最新和兼容版本:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
➜ app_a git:(master) ✗ flutter pub outdated Showing outdated packages. [*] indicates versions that are not the latest available. Package Name Current Upgradable Resolvable Latest direct dependencies: flutter_bloc *7.0.0 7.1.0 7.1.0 7.1.0 shared_preferences *2.0.0 2.0.6 2.0.6 2.0.6 transitive dependencies: meta *1.3.0 *1.3.0 *1.3.0 1.7.0 2 upgradable dependencies are locked (in pubspec.lock) to older versions. To update these dependencies, use `flutter pub upgrade`. |
现在我们可能会问:为什么有一个锁文件这么重要?
答案很简单:
如果我们正在开发一个应用程序,该lock文件保证每个有权访问该项目的人都能够使用我们正在使用的相同版本的库来运行它。它是我们使用的真实版本的“真相来源”。
这意味着即使我们使用^符号,我们也不会在不同机器上有相同依赖项的不同解析版本,这使得与其他开发人员并行开发项目变得更容易。
但是,如果我们查看Dart 官方文档中的What not to commit,我们会看到不应提交pubspec.lock文件:
不要将库包的锁文件检查到源代码管理中,因为库应该支持一系列依赖版本。库包的直接依赖项的版本约束应尽可能宽,同时仍确保依赖项将与测试的版本兼容。
了解这个pubspec.lock文件已经很长时间了,但我们已经成功了
我们现在可以清楚地看到它的重要性——有一种明确的方法来查明我们的应用程序的依赖关系。
此外,我们看到了为什么^在声明依赖项时应该考虑使用该符号 - 它使我们更加灵活,具有更少的依赖项错误,并尝试我们正在使用的库的更新版本。
但是,在本文中,我们只探讨了使用 pub.dev 中的依赖项。如果我们考虑使用我们自己的git依赖项,我们将不仅知道version提交哈希,而且知道提交哈希。通过让我们回答一个非常重要的问题,这些信息可以为我们节省无数的调试时间 - 应用程序无法运行是因为我们没有使用每个库的最新版本吗?
在结束之前还有一件事需要考虑——正如Mark O'Sullivan指出的那样,^在其他技术中使用这种符号会导致Node.js 应用程序中的安全漏洞。您可以在 Chris Laughlin 的演讲中了解更多相关信息:您所有的包都属于我们——保护您的 npm 依赖项。
所以现在您知道如何使用pubspec.lock文件的力量——它将成为您项目的真实来源,以及一种共享和保持应用程序锁定在相同库版本中的方法。
早期版本参考 Flutter 1.22最新的多国语支持配置和使用 ,整个的配置过程比较麻烦。
Flutter 2.8.1/Android Studio Bumblebee 2021.1.1 可以通过 Flutter Intl进行简化处理,完成多语言的配置。
Flutter Intl插件安装:
依次点击:Android Studio—>Preference—>Plugins—>Marketplace,搜索Flutter Intl
继续阅读Flutter 2.8.1/Android Studio Bumblebee 2021.1.1多国语支持配置和使用
开发环境:macOS Big Sur (11.6.2)/Flutter 2.8.1/Android Studio Atrctic Fox (2020.3.1 Patch 4)
参照 将Flutter module集成到Android项目(Android Studio Arctic Fox 2020.3.1/Flutter 2.8.1) 建立项目,在执行 Flutter 单元测试代码的时候报错

Flutter 2.5 正式版已于上周正式发布!这是一次重要的版本更新,也是 Flutter 发布历史上各项统计数据排名第二的版本。我们关闭了 4600 个 Issue,合并了 3932 个 PR,它们分别来自 252 个贡献者和 216 个审核者。回顾去年 -- 我们收到来自 1337 个贡献者提交的 21072 个 PR,其中有 15172 个被合并。在详述本次更新的内容之前,我们想强调,Flutter 的首要工作始终是高质量交付开发者们所需要功能。
Flutter 2.5 带来了一些重要的性能和工具改进,以帮助开发者们追踪应用中的性能问题。同时,加入了一些新的功能,包括对 Android 的全屏支持、 对 Material You (也称 v3) 的更多支持、对文本编辑的更新以支持切换键盘快捷键、在 Widget Inspector 中查看 widget 详情、在 Visual Studio Code 项目中添加依赖关系的新支持、从 IntelliJ / Android Studio 的测试运行中获得测试覆盖率信息的新支持,以及一个更贴近 Flutter 应用在真实的使用场景下的应用模板等。这个版本充满了令人兴奋的新更新,让我们开始介绍吧!
该版本进行了一些性能上的改进:首先是一项用于从离线训练运行中连接 Metal 着色器预编译的 PR (#25644),这将最坏情况下的光栅化时间减少了 2/3 (如我们的基准测试所示),将第 99 百分位的帧时间减少了一半。我们在减少 iOS 卡顿方面取得了持续性的进展,这也是在这条道路上迈出的另一步。然而,着色器预热只是卡顿的一个来源。在该版本以前,处理来自网络、文件系统、插件或其他 isolate 的异步事件可能导致动画中断,这是另一个卡顿的来源。在该版本中我们对 UI Isolate 的事件循环的调度策略 (#25789) 进行了改进,现在帧处理优先于其他异步事件的处理,在我们的测试中,其导致的卡顿已经被消除。
最近看到有人面试的时候被问到了 “Dart中函数调用是值传递还是引用传递?”
初看感觉跟Java应该是相同的,结果搜索到的链接里面言之凿凿的说是传值操作,顿时感觉有些迷茫。
针对大对象,如果进行拷贝传值,会诱发大量的内存复制操作,这种情况是不可想象的。而基本类型,比如整数,字符,传递地址又是得不偿失的性能损失,明明通过寄存器就可以直接传递参数,却非要从内存中读取一遍,是完全没有必要的。
Dart中,基本数据类型传值,类传引用
类对象传值
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
main(){ Test a = new Test(5); print("a的初始值为:${a.value}"); setValue(a); print("修改后a的值为: ${a.value}"); } class Test{ int value = 1; Test(int newValue){ this.value = newValue; } } setValue(Test s){ print("修改value为100"); s.value = 100; } |
输出结果为:
|
1 2 3 |
a的初始值为:5 修改value为100 修改后a的值为:100 |
基本类型传值
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
main(){ int s = 6; setValue(s); print(s); //输出6,而不是7 } class Test{ int value = 1; Test(int newValue){ this.value = newValue; } } setValue(int s){ s += 1; } |
上面的代码,其实转换成 Java 也是一样的结果。
但是深入思考,在 Flutter 中,上面的结论并不全面,对于异构模型而言,取决于内存架构。
如果是非一致内存架构,则CPU跟GPU之间只能进行拷贝操作。
即使是一致内存架构,也分为物理一致还是软件一致,如果仅仅是软件一致,比如PCIE独立显卡,依旧是只能进行拷贝操作,当然拷贝是显卡驱动完成的。只有物理,驱动都完全支持的情况下,才会出现物理意义上的引用传值的情况。有时候,即是两者都支持,GPU对虚拟内存的支持也是一个决定因素。
还是蛮复杂的。
Dart VM有多种方式去运行Dart代码,比如:
JIT模式运行源码或者Kernal binarysnapshot方式:AOT snapshot 和 AppJIT shanpshot两者的主要区别在于VM将Dart源码转换成可执行代码的时机和方式。

VM中的任何Dart代码都是运行在隔离的isolate当中,isolate具有自己的内存(堆)和线程控制的隔离运行环境。VM可以同时具有多个isolate执行Dart的代码,但不同的isolate之间不能直接共享任何的状态,只能通过消息端口来进行通信。 我们所说的线程和isolate之间的关系其实有点模糊,而且isolate也比较依赖VM是怎样嵌入到应用程序当中的。线程和isolate有下面这样的一些限制:
isolate,如果线程要进入另一个isolate就要先退出当前的mutator线程和isolate相关联,mutator是用来执行Dart代码和调用VM API的线程所以一个线程只能进入一个isolate执行Dart代码,退出之后才能进入另一个isolate。 不同的线程也能进入同一个isolate,但不能同时。
当然除了拥有一个mutator线程之外,isolate还可以有多个helper线程,比如:
JIT编译线程GC线程GC标记线程VM内部使用了线程池(dart::ThreadPool)来管理系统的线程,而且内部是基于 dart::ThreadPool::Task 的概念去构建的,而不是直接使用的系统线程。例如,GC的过程就是生成一个 dart::ConcurrentSweeperTask 丢给VM的线程池去处理,而不是使用一个专门的线程来做垃圾回收,线程池可以选择一个空闲的线程或者在没有空闲线程的时候新建一个线程来处理这个任务。类似的,消息循环的处理也并没有使用一个专门的event loop线程,而是在有新消息的时候产生一个 dart::MessageHandlerTask 给线程池。
你可以在命令行下直接给Dart的源码去执行,例如:
|
1 2 |
// hello.dart main() => print('Hello, World!'); |
|
1 2 |
$ dart hello.dart Hello, World! |
事实上Dart 2 VM之后就不再支持直接运行Dart源码了,VM使用了一种Kernel binaries(也就是 dill 文件)包含了序列化的 Kernel ASTs。所以源代码要先经过通用前端 common front-end (CFE) 处理成Kernel AST,而CFE是用Dart写的,可以给VM/dart2js/Dart Dev Compiler这些不同的Dart工具使用。

那么为了保持直接执行Dart源码的便捷性,所以有一个叫做kernel service的isolate,负责将Dart源码处理成Kernel,VM再将Kernel binary拿去运行。

但是CFE和用户的Dart代码是可以在不同的设备上执行,例如在Flutter当中,就是将Dart代码编译成Kernel,和执行Kernel的过程个隔离开来,编译Dart源码的步骤放在了用户的开发机上,执行Kernel放在了移动设备上,Flutter tools负责从开发机上将Kernel binary发送到移动设备上。

flutter tool并不能自己解析Dart源码,它使用了一个叫frontend_server的处理,frontend_server实际上就是CFE的封装和Flutter上特定的Kernel-to-Kernel的转换。frontend_server编译Dart源码到Kernel文件,flutter tools将它同步到执行设备上。Flutter的hot reload也正是依赖frontend_server的,frontend_server在hot reload的过程中能够重用之前编译中的CFE状态,只重编已经更改了的部分。
只有Kernel binary能够被VM加载,并解析创建各种对象。不过这个过程是懒加载的,只有被使用到的库和类的信息才会被装载。每一个程序的实体都会保留指向对应Kernel binary的指针,在需要的时候可以去加载更多的信息。

类的信息只有在被使用的过程中(例如:查找类的成员,或新建对象)才会被完全反序列化出来,从Kernel binary读取类的成员信息,但是函数只会反序列化出函数签名信息,函数体只有在被调用运行的时候才会进一步反序列化出来。

从Kernel binary加载了足够多的信息供运行时成功解析和调用方法之后,就会去解析和调用到main函数了。
程序运行的最初所有的函数主体都不是实际可执行的代码,而是一个占位符,指向LazyCompileStub,它只是简单的要求运行时系统为当前的函数生成可执行的代码,然后尾部调用新生成的代码。

首次编译函数时,是通过未优化编译器来完成的。

未优化编译器通过两个步骤来生成机器码:
AST进行遍历,以生成函数主体的控制流程图CFG。CFG由填充了中间语言IL指令的基本块组成。这里使用的IL指令类似于基于堆栈的虚拟机的指令:从堆栈中获取操作数,执行操作,然后将结果压入同一堆栈。CFG使用一对多的低级IL指令直接生成机器码:每条IL指令扩展为多条机器指令这个过程中还没有执行优化,未优化编译器的目标是快速的生成可执行指令。这也意味着不会尝试静态解析任何未从Kernel binary文件中加载的调用,所以调用的编译是动态完成的。VM在这个时候还不能使用任何基于vitual table或者interface table的调度,而是使用inline caching实现动态调用的。
inline caching的核心是在调用的时候缓存对应方法解析的结果,VM使用的inline caching机制包括:
stub,实现方法调用的快速路径,stub在给定的缓存中查找是否有和接收者匹配的类型,如果找到了增加相应的频次计数器,并且尾部调用缓存的方法;否则,stub调用系统的查找解析逻辑,如果解析成功就更新缓存,并且后续的调用使用对应缓存的方法。下图说明了inline cache在animal.toFace()调用时的关系和状态,使用Dog实例调用两次,Cat实例调用一次:

未优化的编译器足以执行所有的Dart代码,只是它的执行速度会慢一些,所以呢VM还需要实现自适应的优化编译路径,自适应的优化是采用程序运行时的信息去驱动优化策略。未优化的代码在运行时会收集以下信息:
Inline caches过程中每一个方法调用接受的类型信息当某个函数的执行计数器达到某个阈值,这个函数就会提交给后台优化编译器进行优化。
优化编译的方式和未优化编译有点类似,通过遍历序列化的Kernel AST为正在优化的函数构建未优化的IL,不同的是与其直接将IL转换为机器码,优化编译器会将未优化的IL转换成基于static single assignment (SSA)的优化IL。基于SSA的IL根据收集到的类型信息,经典的优化手段和Dart的特殊优化:比如,inlining, range analysis, type propagation, representation selection, store-to-load and load-to-load forwarding, global value numbering, allocation sinking, etc。最后,使用线性扫描寄存器分配器和简单的一对多的IL指令,将优化的IL降低为机器码。
编译完成之后后端编译器请求mutator线程进入一个安全点(safepoint)并且将优化的代码attaches到对应的调用函数上,下次调用该函数的时候就能直接使用优化的代码。

需要注意的是,由优化编译器生成的代码是基于运行时收集到的特定信息完成的,例如一个接受动态类型的函数调用,只接收到某个特定的类型,就会被转换成直接的调用,然后检查接收到的类型是否一致。但是在程序的执行过程中,有可能接收到的类型是其他的。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void printAnimal(obj) { print('Animal {'); print(' ${obj.toString()}'); print('}'); } // Call printAnimal(...) a lot of times with an intance of Cat. // As a result printAnimal(...) will be optimized under the // assumption that obj is always a Cat. for (var i = 0; i < 50000; i++) printAnimal(Cat()); // Now call printAnimal(...) with a Dog - optimized version // can not handle such an object, because it was // compiled under assumption that obj is always a Cat. // This leads to deoptimization. printAnimal(Dog()); |
优化代码是基于运行时信息对输入做了一些假设而产生的,如果在后续的运行过程中输入和假设不匹配,它就要防止违反这些假设,并且能够在违反的情况能够恢复正常运行。这个过程就叫着反优化:只要优化版本遇到无法处理的情况,它就会将执行转移到未优化函数的匹配点并继续运行。未优化的版本不做任何假设,可以处理所有可能的输入。
VM通常会在反优化后放弃优化的版本,然后在以后使用更新的类型反馈再次对其进行优化。VM防止违反优化假设一般有两种方式:
Inline checks (e.g. CheckSmi, CheckClass IL instructions)验证输入是否符合优化。例如,将动态调用转换为直接调用时,编译器会在直接调用之前添加这些检查。在此类检查中发生的反优化称为eager deoptimization,因为它很容易在 check 的时候被检测出来。frames将被标记,并且在执行返回时将对其进行反优化。这种反优化也称为延迟反优化:因为它会延迟到控制权返回到优化代码为止。VM有能力序列化isolate堆上的对象为二进制的snapshot文件,并且可以使用snapshot重新创建相同状态的isolate.

snapshot针对启动速度做了相应的优化,本质上是要创建的对象的列表和他们之间关系。相对于解析Dart源码并逐步创建VM内部的数据结构,VM可以将isolate所必须的数据结构全部打包在snapshot中。
但最初snapshot是不包括机器码的,在后来开发AOT编译的时候就加上去了,开发AOT编译和带机器码的snapshot是为了允许VM在一些无法JIT的平台上运行。带代码的snapshot几乎和普通的snapshot的工作方式是一样的,只是它带有一个代码块,这部分是不需要反序列化的,代码块可以直接map进堆内存。

AppJIT snapshot可以减少大型Dart应用(比如:dartanalyzer 或者 dart2js)的JIT预热时间,在小型应用和VM使用JIT编译的时间差不多。
AppJIT snapshots其实是VM使用一些模拟的数据来训练程序,然后将生成的代码和VM内部的数据结构序列化而生成的,然后分发这个snapshot而不是源码或者Kernel binary。VM使用这个snapshot仍然可以在实际运行的过程中发现数据不匹配训练时而启用JIT。

AOT snapshot最初是为了无法进行JIT编译的平台而引入的,但也可以用来优化启动速度。无法进行JIT就意味着:
AOT snapshot必须包含在应用程序执行期间可以调用的每个功能的可执行代码为了满足这些要求,AOT编译过程中会进行全局静态分析(type flow analysis or TFA),以从已知的入口点确定应用程序的哪些部分是被使用的,分配了哪些类以及类型是如何在程序中传递的。所有这些分析都是保守的,因为必须要保证正确性,有可能会牺牲一点性能,这跟JIT不太一样,JIT生成的代码还可以通过反优化来回到未优化的代码上运行。然后所有可达的代码块都将被编译成机器码,不会再进行任何的类型推测的优化。编译完所有的代码块之后,就可以获得堆的快照了。
然后,可以使用预编译的运行时来运行生成的snapshot,该运行时是Dart VM的特殊变体,其中不包括诸如JIT和动态代码加载工具之类的组件。

即使进行了全局和局部分析,AOT编译的代码仍可能包含无法静态虚拟化的调用操作。为了弥补这种情况,运行时使用了类似JIT过程中的inline cache,在这里叫着switchable calls. JIT部分上面讲过了,inline cache主要包括两部分,一个缓存对象(通常是 dart::UntaggedICData )和一个VM的调用(例如:InlineCacheStub),在JIT模式下运行时只会更新 cache 的缓存,但是在AOT中,运行时可以根据inline cache的状态选择替换缓存和要调用的VM函数路径。

所有的动态调用最初都是unlinked状态,首次调用时会触发UnlinkedCallStub的调用,它又会调用DRT_UnlinkedCall去 link 当前的调用点。 如果DRT_UnlinkedCall尝试将调用点的状态切换为monomorphic,在这个状态下调用就会被替换成直接调用,它通过一个特殊的入口进入方法,并且在入口处验证类型。

在上图的例子中,当 obj.method() 首次执行时,obj 是 C 的实例,那么 obj.method 就会被解析成 C.method,下一次出现同样的调用就会直接调用到 C.method,跳过方法查找的过程。但是进入 C.method 仍然是通过一个特殊的入口进入的,验证 obj 是 C 的实例;如果不是的话,DRT_MonomorphicMiss 就会被调用尝试去进入下一个状态。C.method 有可能仍然是调用的目标函数,例如,obj 是类D的实例,D继承C并且没有overrideC.method。在这种情况下,我们检查是否可以进入single target状态,由 SingleTargetCallStub 实现(也可查看 dart::UntaggedSingleTargetCache)。

在AOT编译过程中,大部分类会在继承结构的深度优先遍历过程分配一个 ID,如果类C具有D0..Dn这些子类,而且都没有override C.method,那么C.:cid <= classId(obj) <= max(D0.:cid, ..., Dn.:cid)表示 obj.method 会被解析成 C.method。在这种情况下,与其进行单态类(monomorphic状态)的比较,我们可以使用类的 ID 范围去检查C的所有子类。
换言之,调用的时候会使用线行扫描inline cache, 类似JIT模式(查看ICCallThroughCodeStub, dart::UntaggedICData 以及 dart::PatchableCallHandler::DoMegamorphicMiss)

当然,如果线性数组中的检查数量超过阈值,将切换为使用类似字典的数据结构。(查看 MegamorphicCallStub, dart::UntaggedMegamorphicCache 以及 dart::PatchableCallHandler::DoMegamorphicMiss)

准备同步代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ cd ~ $ mkdir flutter_engine $ cd flutter_engine # 检出构建依赖 # git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git # 国内用户可从gitlab中检出 $ git clone https://gitlab.com/chromiumsrc/depot_tools.git # 添加到搜索路径 $ export PATH=$PATH:`pwd`/depot_tools |
生成配置代码同步配置文件:
|
1 |
$ vim .gclient |
内容如下:
|
1 2 3 4 5 6 7 8 9 10 |
solutions = [ { "managed": False, "name": "src/flutter", "url": "https://github.com/flutter/engine.git", "custom_deps": {}, "deps_file": "DEPS", "safesync_url": "", }, ] |
注意,上面的 name 字段不能变更,否则会在同步代码的时候报错
|
1 2 3 4 |
________ running 'python3 src/flutter/tools/pub_get_offline.py' in '~/flutter_engine' /usr/local/bin/python3: can't open file '~/flutter_engine/src/flutter/tools/pub_get_offline.py': [Errno 2] No such file or directory Error: Command 'python3 src/flutter/tools/pub_get_offline.py' returned non-zero exit status 2 in ~/flutter_src /usr/local/bin/python3: can't open file '~/flutter_engine/src/flutter/tools/pub_get_offline.py': [Errno 2] No such file or directory |
执行代码同步命令(国内需要配置代理):
|
1 2 3 4 5 6 7 8 9 |
$ gclient sync -D --force --reset # 同步时间超长,要花费几个小时,同步代码约16-17GB,压缩之后约8.75GB # 如果已经执行过同步,代码会被锁定到以前拉取到的某个特定版本。 # 如果想更新到主分钟的最新代码,目前没有很好的办法,只能先整个删除src目录,再次执行同步命令 # 当然,如果需要更新flutter的代码,相关依赖不更新的情况下, # 也可以直接在src/flutter目录下执行git pull 命令来手动更新 |
从源代码开始构建:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ cd src $ ./flutter/tools/gn --ios --unoptimized # 编译配置文件生成在out目录下 # 执行编译 $ ninja -C out/ios_debug_unopt $ ./flutter/tools/gn --android --unoptimized $ ninja -C out/android_debug_unopt # release $ ./flutter/tools/gn --android --runtime-mode=release $ ninja -C out/android_release # 生成的 libflutter.so 很大,被压缩后的在 lib.stripped 目录下,但是这个版本的符号还在,进一步压缩可以移除符号 |
一般情况下我们使用的是官方版本的引擎,如果需要调试 libflutter.so 里面的符号,可以在flutter_infra页面直接下载带有符号表的SO文件,根据打包时使用的Flutter工具版本下载对应的文件即可。
比如2.8.1版本:
|
1 2 3 4 5 6 7 8 |
$ flutter --version # version命令可以看到Engine对应的版本 06afdfe54e Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git Framework • revision 77d935af4d (3 周前) • 2021-12-16 08:37:33 -0800 Engine • revision 890a5fca2e Tools • Dart 2.15.1 $ cat `dirname $(which flutter)`/internal/engine.version # flutter安装目录下的engine.version文件也可以看到完整的版本信息 890a5fca2e34db413be624fc83aeea8e61d42ce6 890a5fca2e34db413be624fc83aeea8e61d42ce6 |
拿到引擎版本号后在https://console.cloud.google.com/storage/browser/flutter_infra_release/flutter/890a5fca2e34db413be624fc83aeea8e61d42ce6/ 看到该版本对应的所有构建产物,下载android-arm-release、android-arm64-release和android-x86目录下的symbols.zip。
使用过 Flutter 构建 APP 的人可能有个疑惑,Flutter 的编译产物有时候是一个共享库,有时候是几个文件,比较难于理解,本文主要介绍 Flutter 中的编译模式。
源代码需要编译才能运行,一般来讲编译模式分为 JIT 和 AOT 两大类
全称为 Just In Time(即时编译),比较典型的就是 V8 JS引擎,它能够即时的编译和运行 JavaScript 代码。你只要输入 JavaScript 的字符串源码,V8 就能编译和运行这段代码。通常来说,支持 JIT 的语言一般能够支持自省函数(eval),在运行时动态的执行源码。
所以 JIT 模式有个显然的优点,你可以直接将代码分发给用户,而不用考虑用的机器架构。有这个,就可以为用户提供丰富和动态的内容。
但缺点也是比较明显的,大量的字符串源码,将花费 JIT 编译器大量的时间和内存来编译和执行,会让用户感觉应用启动缓慢。
全称为 Ahead Of Time(事前编译), 典型的例子就是像 C/C++ 需要被编译成特殊的二进制,才可以通过进程加载和运行。
AOT 的优势就是速度快,通过事先编译好的二进制代码,加载和执行的速度都会非常快,在密集计算或者图形渲染的场景下能够获得比较好的用户体验。
但 AOT 也有一些缺点,编译源代码的时候,需要注意用户的设备架构。对于不同的构架需要生成不同的二进制代码,也就会增加应用需要下载的程序包大小。而且二进制代码需要获得执行权限,所以无法在权限比较严格的系统(比如iOS)中动态更新。
Flutter 使用 Dart 作为开发语言,自然和 Dart 的编译模式脱离不了关系,所以我们先来看一下 Dart 的编译模式。
Script: 最常见的 JIT 模式,就像 Node.js 那样,可以在命令行直接执行 Dart 源代码
Script Snapshot: 不同于 Script 模式,Script Snapshot 模式载入的是已经 token 化的 dart 源码,提前做了编译期间的 Lexer 步骤, 也是属于 JIT 模式
Application Snapshot: JIT 模式,这种模式来源于 dart vm直接载入源码后 dump 出数据。dart vm 通过这种数据启动会更快。不过这种模式是区分架构的,在 X64 下生成的无法给 IA_32 用
AOT: AOT 模式,dart 源代码会被编译成汇编文件,汇编再经过汇编器生成不同架构下的二进制代码
用表格总结一下:

Flutter 程序完全用 Dart, 理论上 Flutter 的编译模式应该和 Dart 的一致,事实上因为 Android 和 iOS 生态的差异,Flutter 衍生了不同的编译模式。
Script: 和 Dart 的 Script 模式一样,但是没有开启使用
Script Snapshot: 也是和 Dart 的 Script Snapshot 模式一样,也没有开启使用
Kernel Snapshot: Dart 的 bytecode 模式,在某种程度上类似 JVM。在 Flutter 项目中也被叫做 Core Snapshot,它是和设备架构无关的
Core JIT: 一种编译后的二进制格式,程序的数据和指令被打包成特殊的二进制,在运行时加载。事实上Core JIT 也被叫做 AOT Blobs, 是 AOT 的一种
AOT Assembly: 和 Dart 的 AOT 模式一样
所以在 Flutter 里会更复杂一点,我们结合 Flutter 的各个开发阶段来解读一下。
开发 Flutter APP 的时候,我们需要 HOT Reload 方便 UI 快速成型,同时也需要比较高的性能来进行视图渲染,所以 Flutter 在 Debug 下使用了 Kernel Snapshot 编译模式,会生成如下产物:
isolate_snapshot_data: 加速 isolate 启动的数据,和业务无关
vm_snapshot_data: 加速 Dart VM 启动的数据,和业务无关
kernel_blob.bin: 业务代码产物

在生产阶段,应用需要非常快的速度。所以 Flutter 使用了 AOT 编译模式,但是在不同的平台下,还是有些不一样的

在 iOS 平台由于 App Store 审核条例不允许动态下发可执行二进制代码,所以在 iOS 上,除了 JavaScript,其他语言都使用 AOT 编译。
但是在 Android 平台上,Core JIT 和 AOT Assembly 都支持了,在 Core JIT 模式下会生成四个产物:isolate_snapshot_data/vm_snapshot_data/isolate_snapshot_instr/vm_snapshot_instr
vm_snapshot_instr 和 isolate_snapshot_instr 是 Dart VM 和 isolate 的指令,在载入后,直接将该块内存执行即可。
isolate_snapshot_data 和 vm_snapshot_data 是 Dart VM 和 isolate 的启动数据,主要是为了加速启动速度。
Flutter Engine 包含了 Dart 的运行时,Flutter 应用的编译产物必须和 Engine 匹配才行,Engine 在不同的阶段提供了不同的支持

通过 gen_snapshot -h 也可以查看到,Engine 对编译模式的支持,事实上 Dart 源码就是经过 gen_snapshot 去处理的。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
To create a core snapshot: --snapshot_kind=core --vm_snapshot_data=<output-file> --isolate_snapshot_data=<output-file> <dart-kernel-file> To create an AOT application snapshot as assembly suitable for compilation as a static or dynamic library: --snapshot_kind=app-aot-assembly --assembly=<output-file> [--obfuscate] [--save-obfuscation-map=<map-filename>] <dart-kernel-file> To create an AOT application snapshot as an ELF shared library: --snapshot_kind=app-aot-elf --elf=<output-file> [--strip] [--obfuscate] [--save-obfuscation-map=<map-filename>] <dart-kernel-file> AOT snapshots can be obfuscated: that is all identifiers will be renamed during compilation. This mode is enabled with --obfuscate flag. Mapping between original and obfuscated names can be serialized as a JSON array using --save-obfuscation-map=<filename> option. See dartbug.com/30524 for implementation details and limitations of the obfuscation pass. |
AOT snapshots 也可以使用 obfuscated 选项开启混淆, --save-obfuscation-map=<filename> 保存符号 mapping 文件。
