Flutter (2.10.1~3.3.9)/Xcode 13.2.1/macOS Big Sur 11.6.4在iPad Pro iOS 15.3.1/iPhone SE 3 iOS 16.1.1真机免费调试

系统与开发环境 Flutter (2.10.1~3.3.9)/Xcode 13.2.1(13C100)/macOS Big Sur 11.6.4/iPad Pro(Model A1673) iOS 15.3.1/iPhone SE 3 iOS 16.1.1

操作步骤

1. 苹果开发网站注册或关联开发者账号,如果暂时不需要发布应用到 Mac App Store,只是在设备上调试应用,则不需要注册收费用户,只需要注册或者关联账号即可。具体可以查看官方介绍 选择会员资格

2. 在 iPad Promacbook Pro 登陆同一个注册的开发者的账号。

3. 通过 USB 数据线把 iPad Promacbook Pro 设备连接起来,如下图:

继续阅读Flutter (2.10.1~3.3.9)/Xcode 13.2.1/macOS Big Sur 11.6.4在iPad Pro iOS 15.3.1/iPhone SE 3 iOS 16.1.1真机免费调试

Flutter线程模型&单例

Flutter线程模型

Flutter Engine自己不创建管理线程。Flutter Engine线程的创建和管理是由embedder负责的。Embeder指的是将引擎移植到平台的中间层代码。

Flutter Engine要求Embeder提供四个Task Runner。尽管Flutter Engine不在乎Runner具体跑在哪个线程,但是它需要线程配置在整一个生命周期里面保持稳定。也就是说一个Runner最好始终保持在同一线程运行。这四个主要的Task Runner包括:

Platform Task Runner

Flutter Engine的主Task Runner,运行Platform Task Runner的线程可以理解为是主线程。类似于Android Main Thread或者iOS的Main Thread。但是我们要注意Platform Task Runner和iOS之类的主线程还是有区别的。

对于Flutter Engine来说Platform Runner所在的线程跟其它线程并没有实质上的区别,只不过我们人为赋予它特定的含义便于理解区分。实际上我们可以同时启动多个Engine实例,每个Engine对应一个Platform Runner,每个Runner跑在各自的线程里。这也是Fuchsia(Google正在开发的操作系统)里Content Handler的工作原理。一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。

跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟iOS UI相关的操作都必须在主线程进行相类似。需要注意的是在Flutter Engine中有很多模块都是非线程安全的。一旦引擎正常启动运行起来,所有引擎API调用都将在Platform Thread里发生。

Platform Runner所在的Thread不仅仅处理与Engine交互,它还处理来自平台的消息。这样的处理比较方便的,因为几乎所有引擎的调用都只有在Platform Thread进行才能是安全的,Native Plugins不必要做额外的线程操作就可以保证操作能够在Platform Thread进行。如果Plugin自己启动了额外的线程,那么它需要负责将返回结果派发回Platform Thread以便Dart能够安全地处理。规则很简单,对于Flutter Engine的接口调用都需保证在Platform Thread进行。

需要注意的是,阻塞Platform Thread不会直接导致Flutter应用的卡顿(跟iOS android主线程不同)。尽管如此,平台对Platform Thread还是有强制执行限制。所以建议复杂计算逻辑操作不要放在Platform Thread而是放在其它线程(不包括我们现在讨论的这个四个线程)。其他线程处理完毕后将结果转发回Platform Thread。长时间卡住Platform Thread应用有可能会被系统Watchdog强行杀死。

UI Task Runner Thread(Dart Runner)

UI Task Runner被Flutter Engine用于执行Dart root isolate代码。Root isolate比较特殊,它绑定了不少Flutter需要的函数方法。Root isolate运行应用的main code。引擎启动的时候为其增加了必要的绑定,使其具备调度提交渲染帧的能力。对于每一帧,引擎要做的事情有:
- Root isolate通知Flutter Engine有帧需要渲染。
- Flutter Engine通知平台,需要在下一个vsync的时候得到通知。
- 平台等待下一个vsync
- 对创建的对象和Widgets进行Layout并生成一个Layer Tree,这个Tree马上被提交给Flutter Engine。当前阶段没有进行任何光栅化,这个步骤仅是生成了对需要绘制内容的描述。
- 创建或者更新Tree,这个Tree包含了用于屏幕上显示Widgets的语义信息。这个东西主要用于平台相关的辅助Accessibility元素的配置和渲染。

除了渲染相关逻辑之外Root Isolate还是处理来自Native Plugins的消息响应,Timers,Microtasks和异步IO。
我们看到Root Isolate负责创建管理的Layer Tree最终决定什么内容要绘制到屏幕上。因此这个线程的过载会直接导致卡顿掉帧。
如果确实有无法避免的繁重计算,建议将其放到独立的Isolate去执行,比如使用compute关键字或者放到非Root Isolate,这样可以避免应用UI卡顿。但是需要注意的是非Root Isolate缺少Flutter引擎需要的一些函数绑定,你无法在这个Isolate直接与Flutter Engine交互。所以只在需要大量计算的时候采用独立Isolate。

Raster(GPU) Task Runner

Raster(GPU) Task Runner被用于执行设备GPU的相关调用。UI Task Runner创建的Layer Tree信息是平台不相关,也就是说Layer Tree提供了绘制所需要的信息,具体如何实现绘制取决于具体平台和方式,可以是OpenGL,Vulkan,软件绘制或者其他Skia配置的绘图实现。GPU Task Runner中的模块负责将Layer Tree提供的信息转化为实际的GPU指令。Raster(GPU) Task Runner同时也负责配置管理每一帧绘制所需要的GPU资源,这包括平台Framebuffer的创建,Surface生命周期管理,保证Texture和Buffers在绘制的时候是可用的。

基于Layer Tree的处理时长和GPU帧显示到屏幕的耗时,Raster(GPU) Task Runner可能会延迟下一帧在UI Task Runner的调度。一般来说UI Runner和GPU Runner跑在不同的线程。存在这种可能,UI Runner在已经准备好了下一帧的情况下,Raster(GPU) Runner却还正在向GPU提交上一帧。这种延迟调度机制确保不让UI Runner分配过多的任务给GPU Runner。

前面我们提到Raster(GPU) Runner可以导致UI Runner的帧调度的延迟,Raster(GPU) Runner的过载会导致Flutter应用的卡顿。一般来说用户没有机会向Raster(GPU) Runner直接提交任务,因为平台和Dart代码都无法跑进Raster(GPU) Runner。但是Embeder还是可以向Raster(GPU) Runner提交任务的。因此建议为每一个Engine实例都新建一个专用的Raster(GPU) Runner线程。

IO Task Runner

前面讨论的几个Runner对于执行任务的类型都有比较强的限制。Platform Runner过载可能导致系统WatchDog强杀,UI和GPU Runner过载则可能导致Flutter应用的卡顿。但是GPU线程有一些必要操作是比较耗时间的,比如IO,而这些操作正是IO Runner需要处理的。

IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备。在Texture的准备过程中,IO Runner首先要读取压缩的图片二进制数据(比如PNG,JPEG),将其解压转换成GPU能够处理的格式然后将数据上传到GPU。这些复杂操作如果跑在GPU线程的话会导致Flutter应用UI卡顿。但是只有GPU Runner能够访问GPU,所以IO Runner模块在引擎启动的时候配置了一个特殊的Context,这个Context跟GPU Runner使用的Context在同一个ShareGroup。事实上图片数据的读取和解压是可以放到一个线程池里面去做的,但是这个Context的访问只能在特定线程才能保证安全。这也是为什么需要有一个专门的Runner来处理IO任务的原因。获取诸如ui.Image这样的资源只有通过async call,当这个调用发生的时候Flutter Framework告诉IO Runner进行刚刚提到的那些图片异步操作。这样GPU Runner可以使用IO Runner准备好的图片数据而不用进行额外的操作。

用户操作,无论是Dart Code还是Native Plugins都是没有办法直接访问IO Runner。尽管Embeder可以将一些一般复杂任务调度到IO Runner,这不会直接导致Flutter应用卡顿,但是可能会导致图片和其它一些资源加载的延迟间接影响性能。所以建议为IO Runner创建一个专用的线程。

各个平台目前默认Runner线程实现

前面我们提到Engine Runner的线程可以按照实际情况进行配置,各个平台目前有自己的实现策略。

iOS和Android

Mobile平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。
所有Engine实例共享同一个Platform Task Runner和Platform Thread。
从 Flutter 2.10 开始,可以使用任何一个有 Task Queue  的线程作为 Platform Task Runner,默认情况下还是使用应用的主线程作为 Platform Task Runner。
Platform Task Runner为什么一定要使用平台的主线程? 因为Platform Task Runner的功能是要处理平台的消息,但是平台的API 绝大多数 都只能在主线程调用(尤其是UI相关的API以及触摸事件),所以Platform Task Runner运行所在的平台的线程必须是主线程。这也就是为什么全部实例共享同一个Platform Task Runner的根本原因。

Fuchsia

每一个Engine实例都为UI,GPU,IO,Platform Runner创建各自新的线程。

上面的线程模型介绍是为了我们后续实现整个应用的单例对象进行知识储备。

接下来,我们需要知道我们的代码默认运行在哪个线程上面。答案就是 UI Task Runner Thread(Dart Runner),运行 Dart Main Isolate 和应用主要的 Dart 代码

那么为什么不直接复用 Platform Task Runner 呢?其实单独出现 Platform Task Runner 更多的是一种妥协的表现,这个线程不能长时间阻塞,否则会被系统杀死。如果直接复用这个线程,而不是使用其他线程非常容易导致 Platform Task Runner 过载,导致卡顿,另外很多系统API只能通过系统的主线程调用,因此这个 Platform Task Runner 主要用于虚拟机的初始化,虚拟机与系统通信等任务。

对于原生的 Flutter 应用(完全新建的纯 Flutter 工程)来说,整个应用只有一个 Flutter Engine,那么实现全局单例对象就比较简单,只要在代码中声明单例对象,当其他的Isolate需要访问单例对象的时候,与 Dart Main Isolate 通信即可实现对单例对象的访问。

对于通过 集成到现有应用(Add-to-app) 的方式,集成到已经存在的应用的情况来说,则需要分两种情况考虑,如果使用全局共享同一个 Flutter Engine 的方式来说,与原生Flutter 应用的区别不大。

但是当使用多 Flutter Engine 的方式来说,则比较复杂。

首先我们知道在 iOS和Android 平台上,所有Engine实例共享同一个Platform Task Runner和Platform Thread也就是说在 iOS和Android 平台上,不管初始化多少个引擎实例,都有且只有一个相同的 Platform Task Runner 。因此,原理上只要是 Platform Task Runner 线程中实现一个全局或静态对象,然后通过 Platform Task Runner 创建其他线程(isolate), 需要操作单例对象的时候,通过 Platform Task Runner ,就可以在 iOS和Android 平台上实现整个应用全局单例的效果。

但是很遗憾的是,我们没办法直接指定某段代码必须执行在 Platform Task Runner 上。因此,对于多线程安全的全局单例可以使用 平台相关代码,在平台侧实现,然后使用平台相关API进行通信的方式来实现,这样有点类似于服务器上使用 Redis 进行数据存储的逻辑。

参考链接


Flutter 2.8.1本地化/国际化应用程序名称

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 2.8.1本地化/国际化应用程序名称

Flutter项目中的pubspec.lock文件是什么?

每次我们运行一个新的 Flutter 项目并运行它时,我们都会看到一条消息:

问题是:当我们获得新的依赖项时会发生什么?

正如我们在上一篇文章中看到的《深入研究 Flutter Pubspec.yaml 文件》我们有不同的方法来向项目添加依赖项,但我们还没有讨论的一件事是为什么我们有时会添加插入^符号在我们的依赖版本之前:

这个符号有什么作用?它与pubspec.lock文件有什么关系?

灵活的依赖版本

正如Dart官方文档中所见,有几种不同的方式来定义包的版本:

  • any 或为空 - 这将导致包版本被定义为最新版本或确定相对于其他包约束应该使用哪个版本

  • <><=>= - 允许我们选择更低、更高、更低且等于或更高且等于规定版本的版本

但是,如果我们想要有灵活性和一些稳定性的保证,我们应该考虑使用插入^符号。如文档中所定义:

^version 表示/保证向后兼容指定版本的所有版本的范围。

因此,遵循语义版本指南(其中1.2.3是Major 1、Minor 2 和Patch 3),这意味着:

  • 对于没有主要版本的依赖项,例如0.12.30.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

现在,这引出了两个不同的问题:

  • Dart 什么时候更新我们的依赖版本?
  • 它如何为项目选择合适的版本?
  • 在哪里可以找到与应用程序一起使用的特定版本?

要回答这些问题,我们必须了解什么是pubspec.lock文件。

pubspec.lock文件的剖析

与其尝试从整体上查看文件,不如换一种方式——我们创建一个新项目,提交它,然后添加一个新的依赖项:

之后,我们运行flutter pub get并查看pubspec.lock文件的差异内容:

我们一点一点来分析一下:

  • 在顶部,我们有库名称定义,在本例中为version_banner
  • 接着是dependency类型,它告诉我们它是一个direct依赖项,即我们通过pubspec.yaml文件添加的,还是一个transitive依赖项(如package_info锁定文件中的依赖项所示),它是一个依赖项的依赖项;
  • description将相关的依赖性的类型的其他信息(GIT,本地或托管),无论是路径依赖,在git的承诺散列和URL存储库,或托管依赖的URL,在这种情况下, , pub.dev;
  • source告诉我们如何将依赖项添加到项目中。在本例中,我们通过 pub.dev(托管)添加它,但我们也可以使用sourceaspathgit;
  • 最后,version将告诉我们我们正在使用的具体版本是什么。

如果我们仔细查看version,我们会发现虽然我们在pubspec.yamlversion 中定义了^0.2.0,但从锁定文件中解析出来的版本是0.2.1,这意味着 Dart 能够接受并使用最新版本的库,尊重它的次要版本。

为什么我们应该使用^

如果我们只使用一个依赖,使用的好处^只是让我们拥有最新最好的版本。然而,^在我们的依赖项中使用还有另一个很好的理由——它允许 Dart 足够灵活地选择一个与所有瞬态(或依赖项的依赖项)依赖项一致的依赖项版本。

想象一下以下场景 - 您http在应用程序中使用最新版本的包 - http: 0.13.0. 但是,您还使用了另一个名为 library_b 的库,它也依赖于http,但它使用的是更新版本!http: 0.13.3.

那么如果我们选择使用确切的版本会发生什么http: 0.13.3

由于我们使用的是硬依赖版本,Dart 将无法找到同时满足这两个约束的版本。但是如果我们添加^到应用程序的依赖项中呢?从理论上讲,这意味着应用程序会说“我可以有一个灵活的 http 版本,所以找到一个既满足又不引起冲突的版本”。

就这样,我们运行flutter pub get命令:

查看pubspec.lock应用程序的文件,我们看到它现在使用更新版本0.13.3

但是,如果版本不同会发生什么?让我们看看http应用程序版本高于库版本的情况:

该^符号只能找到等于或高于我们声明的版本的版本,这意味着如果我们运行,flutter pub get我们将看到以下错误消息:

解决这个问题的一个唯一的办法就是让这两个库和应用程序依赖于一个^版本,让他们找到一个版本,同时满足。

如果我们无法^在我们正在使用的依赖项中添加表示法,我们总是可以dependency_overrides在我们的pubspec.yaml文件中使用 a ,正如我们在深入研究 Pubspec.yaml 文件中所讨论的那样。

升级我们的版本

因此,如果通过运行flutter pub get我们没有看到通过pubspec.lock文件对我们的包进行任何更新,那么我们如何更新我们的依赖项?

答案是使用flutter pub upgrade

让我们以以下应用程序依赖项为例,并假设pubspec.lock文件使用相同的版本:

如果我们运行,flutter pub upgrade我们会看到所有依赖项都将具有更高版本,符合以下标准^

通过查看差异,我们可以看到这些版本已更新到较新的版本:

但是如果我们不想升级所有的依赖呢?如果我们只想测试特定包的最新版本怎么办?幸运的是,该upgrade命令允许我们使用flutter pub upgrade <package_name>.

让我们通过使用来测试它flutter pub upgrade shared_preferences

正如我们所看到的,命令行告诉我们,虽然有一个更新的版本可用于flutter_bloc,但它没有被应用。我们可以通过查看差异来验证这一点:

查看flutter pub upgrade命令的最后一行,我们看到它提到了该flutter pub outdated命令。如果我们希望pubspec.yaml直接更新我们的文件,此命令可用于检查我们软件包的所有最新和兼容版本:

锁文件在应用中的重要性

现在我们可能会问:为什么有一个锁文件这么重要?

答案很简单:

如果我们正在开发一个应用程序,该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 项目中的pubspec.lock 文件是什么?

Flutter 2.8.1/Android Studio Bumblebee 2021.1.1多国语支持配置和使用

早期版本参考 Flutter 1.22最新的多国语支持配置和使用 ,整个的配置过程比较麻烦。

Flutter 2.8.1/Android Studio Bumblebee 2021.1.1 可以通过 Flutter Intl进行简化处理,完成多语言的配置。

插件安装(Android Studio Bumblebee 2021.1.1)

Flutter Intl插件安装:

依次点击:Android Studio—>Preference—>Plugins—>Marketplace,搜索Flutter Intl

继续阅读Flutter 2.8.1/Android Studio Bumblebee 2021.1.1多国语支持配置和使用

Flutter单元测试报错“Error: Not found: 'dart:ui'”

开发环境: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单元测试报错“Error: Not found: 'dart:ui'”

Flutter 2.5 更新详解

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) 进行了改进,现在帧处理优先于其他异步事件的处理,在我们的测试中,其导致的卡顿已经被消除。

继续阅读Flutter 2.5 更新详解

Dart中函数调用是值传递还是引用传递?

最近看到有人面试的时候被问到了 “Dart中函数调用是值传递还是引用传递?”

初看感觉跟Java应该是相同的,结果搜索到的链接里面言之凿凿的说是传值操作,顿时感觉有些迷茫。

针对大对象,如果进行拷贝传值,会诱发大量的内存复制操作,这种情况是不可想象的。而基本类型,比如整数,字符,传递地址又是得不偿失的性能损失,明明通过寄存器就可以直接传递参数,却非要从内存中读取一遍,是完全没有必要的。

Dart中,基本数据类型传值,类传引用

类对象传值

输出结果为:

基本类型传值

上面的代码,其实转换成 Java 也是一样的结果。

但是深入思考,在 Flutter 中,上面的结论并不全面,对于异构模型而言,取决于内存架构。

如果是非一致内存架构,则CPU跟GPU之间只能进行拷贝操作。

即使是一致内存架构,也分为物理一致还是软件一致,如果仅仅是软件一致,比如PCIE独立显卡,依旧是只能进行拷贝操作,当然拷贝是显卡驱动完成的。只有物理,驱动都完全支持的情况下,才会出现物理意义上的引用传值的情况。有时候,即是两者都支持,GPU对虚拟内存的支持也是一个决定因素。

还是蛮复杂的。

Dart VM是如何运行你的代码的

Dart VM有多种方式去运行Dart代码,比如:

  • JIT模式运行源码或者Kernal binary
  • 通过snapshot方式:AOT snapshot 和 AppJIT shanpshot

两者的主要区别在于VMDart源码转换成可执行代码的时机和方式。

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的源码去执行,例如:


事实上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 serviceisolate,负责将Dart源码处理成KernelVM再将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将它同步到执行设备上。Flutterhot reload也正是依赖frontend_server的,frontend_serverhot reload的过程中能够重用之前编译中的CFE状态,只重编已经更改了的部分。

Kernel binary装载

只有Kernel binary能够被VM加载,并解析创建各种对象。不过这个过程是懒加载的,只有被使用到的库和类的信息才会被装载。每一个程序的实体都会保留指向对应Kernel binary的指针,在需要的时候可以去加载更多的信息。

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

Kernel binary加载了足够多的信息供运行时成功解析和调用方法之后,就会去解析和调用到main函数了。

函数编译

程序运行的最初所有的函数主体都不是实际可执行的代码,而是一个占位符,指向LazyCompileStub,它只是简单的要求运行时系统为当前的函数生成可执行的代码,然后尾部调用新生成的代码。

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

未优化编译器通过两个步骤来生成机器码:

  1. 对函数主体的序列化AST进行遍历,以生成函数主体的控制流程图CFGCFG由填充了中间语言IL指令的基本块组成。这里使用的IL指令类似于基于堆栈的虚拟机的指令:从堆栈中获取操作数,执行操作,然后将结果压入同一堆栈。
  2. CFG使用一对多的低级IL指令直接生成机器码:每条IL指令扩展为多条机器指令

这个过程中还没有执行优化,未优化编译器的目标是快速的生成可执行指令。这也意味着不会尝试静态解析任何未从Kernel binary文件中加载的调用,所以调用的编译是动态完成的。VM在这个时候还不能使用任何基于vitual table或者interface table的调度,而是使用inline caching实现动态调用的。

inline caching的核心是在调用的时候缓存对应方法解析的结果,VM使用的inline caching机制包括:

  • 一个调用的特殊缓存,将接收的类映射到方法,如果接收者具有匹配的类型则调用方法,缓存还会有一些辅助信息,比如:调用频次计数器,跟踪特定类型出现的频次。
  • 一个共享的stub,实现方法调用的快速路径,stub在给定的缓存中查找是否有和接收者匹配的类型,如果找到了增加相应的频次计数器,并且尾部调用缓存的方法;否则,stub调用系统的查找解析逻辑,如果解析成功就更新缓存,并且后续的调用使用对应缓存的方法。

下图说明了inline cacheanimal.toFace()调用时的关系和状态,使用Dog实例调用两次,Cat实例调用一次:

未优化的编译器足以执行所有的Dart代码,只是它的执行速度会慢一些,所以呢VM还需要实现自适应的优化编译路径,自适应的优化是采用程序运行时的信息去驱动优化策略。未优化的代码在运行时会收集以下信息:

  • Inline caches过程中每一个方法调用接受的类型信息
  • 执行计数器收集的热点代码区

当某个函数的执行计数器达到某个阈值,这个函数就会提交给后台优化编译器进行优化。

优化编译

优化编译的方式和未优化编译有点类似,通过遍历序列化的Kernel AST为正在优化的函数构建未优化的IL,不同的是与其直接将IL转换为机器码,优化编译器会将未优化的IL转换成基于static single assignment (SSA)的优化IL。基于SSAIL根据收集到的类型信息,经典的优化手段和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到对应的调用函数上,下次调用该函数的时候就能直接使用优化的代码。

需要注意的是,由优化编译器生成的代码是基于运行时收集到的特定信息完成的,例如一个接受动态类型的函数调用,只接收到某个特定的类型,就会被转换成直接的调用,然后检查接收到的类型是否一致。但是在程序的执行过程中,有可能接收到的类型是其他的。

反优化

优化代码是基于运行时信息对输入做了一些假设而产生的,如果在后续的运行过程中输入和假设不匹配,它就要防止违反这些假设,并且能够在违反的情况能够恢复正常运行。这个过程就叫着反优化:只要优化版本遇到无法处理的情况,它就会将执行转移到未优化函数的匹配点并继续运行。未优化的版本不做任何假设,可以处理所有可能的输入。

VM通常会在反优化后放弃优化的版本,然后在以后使用更新的类型反馈再次对其进行优化。VM防止违反优化假设一般有两种方式:

  • Inline checks (e.g. CheckSmi, CheckClass IL instructions)验证输入是否符合优化。例如,将动态调用转换为直接调用时,编译器会在直接调用之前添加这些检查。在此类检查中发生的反优化称为eager deoptimization,因为它很容易在 check 的时候被检测出来。
  • 全局保护程序,指令运行时在更改优化代码所依赖的内容时丢弃优化代码。例如,优化编译器可能发现某些类C从未扩展过,并在类型传播过程中使用了此信息。但是,随后的动态代码加载或类最终确定可能会引入C的子类-使得假设无效。这个时候,运行时需要查找并丢弃所有在C没有子类的假设下编译的优化代码。运行时可能会在执行堆栈上找到一些现在无效的优化代码,在这种情况下,受影响的frames将被标记,并且在执行返回时将对其进行反优化。这种反优化也称为延迟反优化:因为它会延迟到控制权返回到优化代码为止。

运行 Snapshots

VM有能力序列化isolate堆上的对象为二进制的snapshot文件,并且可以使用snapshot重新创建相同状态的isolate.

snapshot针对启动速度做了相应的优化,本质上是要创建的对象的列表和他们之间关系。相对于解析Dart源码并逐步创建VM内部的数据结构,VM可以将isolate所必须的数据结构全部打包在snapshot中。

但最初snapshot是不包括机器码的,在后来开发AOT编译的时候就加上去了,开发AOT编译和带机器码的snapshot是为了允许VM在一些无法JIT的平台上运行。带代码的snapshot几乎和普通的snapshot的工作方式是一样的,只是它带有一个代码块,这部分是不需要反序列化的,代码块可以直接map进堆内存。

运行 AppJIT snapshots

AppJIT snapshot可以减少大型Dart应用(比如:dartanalyzer 或者 dart2js)的JIT预热时间,在小型应用和VM使用JIT编译的时间差不多。

AppJIT snapshots其实是VM使用一些模拟的数据来训练程序,然后将生成的代码和VM内部的数据结构序列化而生成的,然后分发这个snapshot而不是源码或者Kernel binaryVM使用这个snapshot仍然可以在实际运行的过程中发现数据不匹配训练时而启用JIT

运行 AppAOT snapshots

AOT snapshot最初是为了无法进行JIT编译的平台而引入的,但也可以用来优化启动速度。无法进行JIT就意味着:

  1. AOT snapshot必须包含在应用程序执行期间可以调用的每个功能的可执行代码
  2. 可执行代码不能基于运行时的数据进行任何的假设

为了满足这些要求,AOT编译过程中会进行全局静态分析(type flow analysis or TFA),以从已知的入口点确定应用程序的哪些部分是被使用的,分配了哪些类以及类型是如何在程序中传递的。所有这些分析都是保守的,因为必须要保证正确性,有可能会牺牲一点性能,这跟JIT不太一样,JIT生成的代码还可以通过反优化来回到未优化的代码上运行。然后所有可达的代码块都将被编译成机器码,不会再进行任何的类型推测的优化。编译完所有的代码块之后,就可以获得堆的快照了。

然后,可以使用预编译的运行时来运行生成的snapshot,该运行时是Dart VM的特殊变体,其中不包括诸如JIT和动态代码加载工具之类的组件。

Switchable Calls

即使进行了全局和局部分析,AOT编译的代码仍可能包含无法静态虚拟化的调用操作。为了弥补这种情况,运行时使用了类似JIT过程中的inline cache,在这里叫着switchable callsJIT部分上面讲过了,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模式(查看ICCallThroughCodeStubdart::UntaggedICData 以及 dart::PatchableCallHandler::DoMegamorphicMiss)

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

参考链接


macOS Big Sur(11.6.2/11.7.1) 编译Flutter engine

准备同步代码:

生成配置代码同步配置文件:

内容如下:

注意,上面的 name 字段不能变更,否则会在同步代码的时候报错

执行代码同步命令(国内需要配置代理):

从源代码开始构建:

一般情况下我们使用的是官方版本的引擎,如果需要调试 libflutter.so 里面的符号,可以在flutter_infra页面直接下载带有符号表的SO文件,根据打包时使用的Flutter工具版本下载对应的文件即可。

比如2.8.1版本:

拿到引擎版本号后在https://console.cloud.google.com/storage/browser/flutter_infra_release/flutter/890a5fca2e34db413be624fc83aeea8e61d42ce6/ 看到该版本对应的所有构建产物,下载android-arm-release、android-arm64-release和android-x86目录下的symbols.zip。

参考链接