From 555d1558c94b52d02bb6ff9641996eac88f156be Mon Sep 17 00:00:00 2001 From: guoshuyu <359369982@qq.com> Date: Mon, 27 Jun 2022 10:34:05 +0800 Subject: [PATCH] update --- .qiniu_pythonsdk_hostscache.json | 2 +- Dart-217.md | 105 +++++ FWREADME.md | 55 +-- Flutter-120HZ.md | 166 ++++++++ Flutter-300.md | 313 +++++++++++++++ Flutter-BIO.md | 272 +++++++++++++ Flutter-BIOS.md | 94 +++++ Flutter-DWN.md | 205 ++++++++++ Flutter-DWW.md | 661 ++++++++++++++++++++++++++++++ Flutter-Extended.md | 205 ++++++++++ Flutter-FF.md | 439 ++++++++++++++++++++ Flutter-GB.md | 279 +++++++++++++ Flutter-N1.md | 179 +++++++++ Flutter-N2.md | 213 ++++++++++ Flutter-N3.md | 197 +++++++++ Flutter-N4.md | 242 +++++++++++ Flutter-N6.md | 234 +++++++++++ Flutter-P3.md | 96 +++++ Flutter-TL.md | 102 +++++ Flutter-Web-T.md | 662 +++++++++++++++++++++++++++++++ README.md | 18 + SUMMARY.md | 38 +- UPDATE.md | 2 + 23 files changed, 4738 insertions(+), 41 deletions(-) create mode 100644 Dart-217.md create mode 100644 Flutter-120HZ.md create mode 100644 Flutter-300.md create mode 100644 Flutter-BIO.md create mode 100644 Flutter-BIOS.md create mode 100644 Flutter-DWN.md create mode 100644 Flutter-DWW.md create mode 100644 Flutter-Extended.md create mode 100644 Flutter-FF.md create mode 100644 Flutter-GB.md create mode 100644 Flutter-N1.md create mode 100644 Flutter-N2.md create mode 100644 Flutter-N3.md create mode 100644 Flutter-N4.md create mode 100644 Flutter-N6.md create mode 100644 Flutter-P3.md create mode 100644 Flutter-TL.md create mode 100644 Flutter-Web-T.md diff --git a/.qiniu_pythonsdk_hostscache.json b/.qiniu_pythonsdk_hostscache.json index 888f785..4feb58d 100644 --- a/.qiniu_pythonsdk_hostscache.json +++ b/.qiniu_pythonsdk_hostscache.json @@ -1 +1 @@ -{"http:V-8fqeyK7oW8wXIqEIi92CZ-ymENQfoEYcqExXAK:carguo": {"upHosts": ["http://up-z2.qiniu.com", "http://upload-z2.qiniu.com", "-H up-z2.qiniu.com http://14.29.110.6"], "ioHosts": ["http://iovip-z2.qbox.me"], "deadline": 1640328057}} \ No newline at end of file +{"http:V-8fqeyK7oW8wXIqEIi92CZ-ymENQfoEYcqExXAK:carguo": {"upHosts": ["http://up-z2.qiniu.com", "http://upload-z2.qiniu.com", "-H up-z2.qiniu.com http://14.29.110.6"], "ioHosts": ["http://iovip-z2.qbox.me"], "deadline": 1656381382}} \ No newline at end of file diff --git a/Dart-217.md b/Dart-217.md new file mode 100644 index 0000000..618de8f --- /dev/null +++ b/Dart-217.md @@ -0,0 +1,105 @@ +# Dart 2.17 正式发布 + +随着 [Flutter 3](https://link.juejin.cn/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FUZJX0HIi7ERW_ZNcz1hflg) 在本次 I/O 大会的发布,我们也同时正式发布了 Dart 2.17 稳定版 SDK。这个版本的发布是围绕着我们的核心主题构建的,即:领先的生产力和平台可移植性。 + +Dart 2.17 提供了新的语言特性:**枚举支持成员变量、改进的超类参数继承,以及更为灵活的命名参数**。我们同时为 `package:lints` 开启了 2.x 版本,这是一套官方的 lint 规则,是根据我们总结的 Dart 最佳实践整合而成的一个 lint 规则集。与此同时,我们也更新了核心库的 API 文档,为其带来了丰富的示例代码。并且,为了改善平台集成特性,我们在 Flutter 插件中提供了一个新的模版,使用 `dart:ffi` 与原生平台进行 C 语言的互操作、对 RISC-V 指令集提供实验性支持,以及对 macOS 和 Windows 可执行文件的签名支持。 + +![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image1) + +## 编程语言新特性助力生产力提升 + +我们一直在持续地改进 Dart 编程语言,不断添加新特性以及改进现有的特性,以助力开发者们工作效率的提升。Dart 2.17 增加了对枚举成员变量的支持,优化了在构造函数中使用命名参数的方式,并且开始使用继承超类的参数以减少冗长和重复的代码。 + +### 增强的支持成员变量的枚举 + +枚举非常适合表示一组离散的状态。例如,我们可以将水描述为 `enum Water { frozen, lukewarm, boiling }`。但如果我们想在 `enum` 上添加一些方法,例如,将每个状态转换为温度,并支持将 `enum` 转换为 `String`,该怎么办?或许我们可以使用扩展方法来添加一个 `waterToTemp()` 方法,但我们必须时刻注意它与 `enum` 的同步。对于 `String` 我们希望覆写 `toString()` 方法,但它不支持这么做。 + +在 Dart 2.17 中现已支持枚举类型的成员变量。这意味着我们可以添加保存状态的字段、设置状态的构造函数、具有功能的方法,甚至覆写现有的方法。社区中许多开发者一直有这样的需求,这是我们在 Dart 编程语言仓库的问题追踪中 [投票排名第三的问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flanguage%2Fissues%3Fq%3Dis%3Aissue%2Bsort%3Areactions-%2B1-desc%2B)。 + +继续拿 `Water` 举例,我们可以添加一个保存温度的 `int` 字段,并添加接收 `int` 的默认构造函数: + +```dart +enum Water + const Water(this.tempInFahrenheit); + + final int tempInFahrenheit; +} +``` + +为了确保在创建枚举时构造函数被正常调用,我们需要为每一个枚举值附以显式的调用: + +```dart +enum Water { + frozen(32), + lukewarm(100), + boiling(212); +} +``` + +想要支持从枚举转换为 `String`,我们可以很简单地覆写 `toString` 方法,因为 `enums` 也继承自 `Object`: + +```dart +@override +String toString() => "The $name water is $tempInFahrenheit F."; +``` + +如此一来,你就有了一个可以轻松实例化完整功能的枚举类,并且可以在任意位置调用方法: + +```dart +void main() { + print(Water.frozen); // 打印内容为 “The frozen water is 32 F.” +} +``` + +这两种方法的完整示例如下所示,有了这些改动,新版本的代码更易于阅读和维护。 + +![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image2) + +### 超类的初始化构造 + +当你的代码存在类型继承关系时,一个常见的做法是将一些构造函数参数传递给超类的构造函数。为此子类需要 1) 在其构造函数中列出每个参数 2) 使用这些参数调用超类的构造函数。这导致了大量的代码重复,使代码难以阅读和维护。 + +几位 Dart 社区成员帮助 Dart 实现了这项语言目标。半年前,GitHub 用户 [@roy-sianez](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Froy-sianez) 提交了一个 [语言问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flanguage%2Fissues%2F1855)。他的建议类似于 GitHub 用户 [@apps-transround](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fapps-transround) 先前的 [建议](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flanguage%2Fissues%2F493%23issuecomment-879624528):也许我们可以通过引入一个新的方式来表示在超类中指定了一个参数,来解决这个问题。我们认为这是一个好主意,因此已将其实现并添加到了 Dart 2.17 版本中。从以下示例中可以看出,这与 Flutter widget 的代码有很强的相关性。实际上当我们将这项特性应用到 Flutter 框架时,我们看到框架总共减少了 [近两千行代码](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fpull%2F100905%2Ffiles)! + +![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image3) + +### 可在任意参数位置使用命名参数 + +最后,我们改进了方法调用时命名参数的方式。在此次更新之前,命名参数的调用必须出现在普通参数列表的后面。当你想要提升代码可读性,希望将命名参数写在靠前的位置但它无法工作时,会觉得非常惆怅。例如下方 `List.generate` 构造函数的调用。此次更新之前 `growable` 参数必须放在最后,这会导致这个参数很容易被可能有很多内容的构造参数所影响而错过。现在你可以根据自己的喜好对它们进行排序,你可以先使用命名参数,最后使用生成器参数。 + +![img](http://img.cdn.guoshuyu.cn/20220627_Dart-217/image4) + +更多有关这三项改进的示例,请参阅我们更新的 [枚举](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsamples%2Fblob%2Fmaster%2Fenhanced_enums%2Flib%2Fmembers.dart)、[超类的初始化构造](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsamples%2Fblob%2Fmaster%2Fparameters%2Flib%2Fsuper_initalizer.dart) 和 [命名参数](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsamples%2Fblob%2Fmaster%2Fparameters%2Flib%2Fnamed_parameters.dart) 示例代码。 + +## 生产力工具改进 + +回到生产力的主题,我们围绕生产力对核心工具进行了一些改进。 + +在 Dart 2.14 中,我们引入了 `package:lints`,它与 Dart 分析器一起工作以防止你编写错误的代码,并使用更规范的规则审查你的 Dart 代码。之后分析器中又新增了许多代码提示规则,我们对其进行了仔细分类,并从中选择了 [10 条新的用于所有 Dart 代码的代码提示规则](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Flints%2Fblob%2Fmain%2FCHANGELOG.md%23200) ,以及 [2 条新的专门用于 Flutter 代码的代码提示规则](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fflutter%2Fpackages%2Fblob%2Fmaster%2Fpackages%2Fflutter_lints%2FCHANGELOG.md%23200)。它们包括确保你导入的 package 中有正确地在你 pubspec 文件中声明、防止滥用对类型参数的空检查以及确保子属性格式一致的代码提示规则。你可以简单地使用命令升级到新的 `lints` package: + +- 对 Dart package 可以使用: `dart pub upgrade —-major-versions lints` +- 对 Flutter package 可以使用: `flutter pub upgrade —-major-versions flutter_lints` + +`SecureSockets` 通常用于启用使用 TLS 和 SSL 保护的 TCP 套接字连接。在 Dart 2.17 之前,因为没有办法检查安全数据流量,在开发过程中调试这些加密连接变得十分棘手。现在我们添加了对指定 `keyLog` 文件的支持,指定后,当与服务器交换新的 TLS 密钥时,[NSS 密钥日志格式](https://link.juejin.cn/?target=https%3A%2F%2Ffirefox-source-docs.mozilla.org%2Fsecurity%2Fnss%2Flegacy%2Fkey_log_format%2Findex.html) 中的一行文本将附加到文件中。这将使网络流量分析工具 (例如 [Wireshark](https://link.juejin.cn/?target=https%3A%2F%2Fgitlab.com%2Fwireshark%2Fwireshark%2F-%2Fwikis%2FTLS%23tls-decryption)) 能够解密通过套接字发送的内容。更多详细信息,请参阅`SecureSocket.connect()` 的 [API 文档](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-io%2FSecureSocket%2Fconnect.html)。 + +`dart doc` 生成的 API 文档是大多数 Dart 开发者学习新 API 的重要内容之一。虽然我们的核心库 API长期以来都有丰富的文本描述,但许多开发者告诉我们,他们更喜欢通过阅读示例代码来学习 API。在 Dart 2.17 中,我们检查了所有主要的核心库,为浏览量排名的前 200 个页面添加了详实的示例代码。你可以对比 `dart:convert` 在 [Dart 2.16](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.16.2%2Fdart-convert%2Fdart-convert-library.html) 和 [2.17](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-convert%2Fdart-convert-library.html) 的文档页面查看这些改变,希望这些改变可以帮助你更好地使用 API 文档。 + +助力生产力的提高不仅是做加法,做减法也同样重要,我们清理了一些堆积的内容,并删除了 SDK 里已弃用的的 API,这将帮助我们保持更小的代码体积,这对新上手的开发者们尤为重要。为此,我们从 `dart:io` 库中删除了 [231 行已弃用的代码](https://link.juejin.cn/?target=https%3A%2F%2Fdart-review.googlesource.com%2Fc%2Fsdk%2F%2B%2F236840)。如果你仍在使用这些已弃用的 API,你可以使用 `dart fix` 进行修复和替换。我们还在继续努力删除 [已弃用的 Dart CLI 工具](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsdk%2Fissues%2F46100),本次更新删除了 `dartdoc` 工具 (使用`dart doc` 代替) 和 `pub` 工具 (使用 `dart pub` 或 `flutter pub` 代替)。 + +## 扩大平台集成和支持 + +第二个核心主题是平台集成和支持。Dart 是一种真正的多平台语言。虽然我们已经支持 [大量的平台](https://link.juejin.cn/?target=https%3A%2F%2Fdart.cn%2Foverview%23platform),但我们仍在不断拓展新平台,以确保你可以与每个受支持的平台深度集成,同时也关注更新兴的平台。 + +我们 [与 C 语言或原生代码互操作](https://link.juejin.cn/?target=https%3A%2F%2Fdart.cn%2Fguides%2Flibraries%2Fc-interop) 的核心机制——Dart FFI,是一种将 Dart 代码与现有原生平台代码集成的流行方式。在 Flutter 上,FFI 是构建使用宿主平台原生 API (例如 Windows win32 API) 插件的好方法。在 Dart 2.17 和 Flutter 3 中,我们向 `flutter` 工具添加了 FFI 的模板,现在你可以轻松地创建 FFI 插件,这些插件具有通过 `dart:ffi` 调用原生代码支持的 Dart API。详细信息请参阅开发者文档 [开发 package 和插件](https://link.juejin.cn/?target=https%3A%2F%2Fflutter.cn%2Fdocs%2Fdevelopment%2Fpackages-and-plugins%2Fdeveloping-packages%23dart-only-platform-implementations) 页面。 + +FFI 现在支持特定于 ABI 的类型,可以在具有特定 [ABI (应用程序二进制接口)](https://link.juejin.cn/?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2FABI%2F10912305) 类型的平台上使用 FFI。例如,现在你可以使用 `Long` (C 语言中的 `long`) 正确表示具有特定于 ABI 大小的长整数,由于 CPU 架构的区别,结果可能是 32 位或 64 位。有关支持类型的完整列表,请参阅 [AbiSpecificInteger API 页面](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-ffi%2FAbiSpecificInteger-class.html) 中的 "Implementers" 列表。 + +在使用 Dart FFI 与原生平台深度集成时,有时需要将 Dart 分配的内存或其他资源 (端口、文件等) 的清理行为与原生代码对齐。长期以来,这个问题都十分棘手,因为 Dart 是一种会自动处理垃圾回收清理行为的语言。在 Dart 2.17 中,我们通过引入 Finalizer 的概念解决了这个问题,它包括一个 `Finalizable` 标记接口,用于「标记」不应过早终结或丢弃的对象,以及一个 `NativeFinalizer` 可以附加到 Dart 对象上,当对象即将被垃圾回收时提供回调运行。Finalizer 让原生代码和 Dart 代码中同时运行清理。更多详细信息请参阅 [NativeFinalizer API 文档](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-ffi%2FNativeFinalizer-class.html) 中的描述和示例,或 [WeakReferences](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-core%2FWeakReference-class.html) 以及 [Finalizer](https://link.juejin.cn/?target=https%3A%2F%2Fapi.dart.cn%2Fstable%2F2.17.0%2Fdart-core%2FFinalizer-class.html) 在 Dart 代码中的类似支持。 + +将 Dart 编译为本机代码的支持,也是使 Flutter 应用具有出色的启动性能和快速渲染的核心。除此之外,你还可以使用 `dart compile` 编译 Dart 文件为可执行文件。这些可执行文件可以在任何机器上独立运行,无需安装 Dart SDK。Dart 2.17 中的另一个新功能是支持对可执行文件进行签名,生成的产物可以在经常需要签名的 Windows 和 macOS 上进行部署。 + +我们还保持在新兴的平台前沿,继续扩大我们所支持的平台集。[RISC-V](https://link.juejin.cn/?target=https%3A%2F%2Friscv.org%2Fabout%2F) 是一个全新的指令集体系。RISC-V International 是一家全球性的非盈利组织,拥有 RISC-V 规范,使得指令集自由开放。这仍然是一个新兴的平台,但我们对其潜力感到兴奋,因此我们的 `2.17.0–266.1.beta` Linux 版本包含了对它的实验性支持。我们希望能够听到你的反馈,你可以 [提出问题](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fdart-lang%2Fsdk%2Fissues) 或 [分享](https://link.juejin.cn/?target=https%3A%2F%2Fgroups.google.com%2Fa%2Fdartlang.org%2Fg%2Fmisc) 你的体验! + +## 开始使用 Dart 2.17! + +我们希望 Dart 的 2.17 正式版能打动你并能助力你提高工作效率,也同时能够把你的应用带去更多的平台。即刻下载 Dart 2.17 并开始使用,也安装使用 Flutter 3,使用内置的 Dart SDK。 \ No newline at end of file diff --git a/FWREADME.md b/FWREADME.md index fb43cd0..b2f8aa9 100644 --- a/FWREADME.md +++ b/FWREADME.md @@ -3,81 +3,58 @@ ### 作为对 Flutter 系列额外的补充,不定时给你带来有趣的文章。 * [Flutter 跨平台框架应用实战-2019极光开发者大会](Flutter-jg-meet.md) - * [Flutter 面试知识点集锦](Flutter-msjj.md) - * [全网最全 Flutter 与 ReactNative深入对比分析](qwzqdb.md) - * [Flutter 开发实战与前景展望 - RTC Dev Meetup](Flutter-rtc-meetup.md) - * [Flutter Interact 的 Flutter 1.12 大进化和回顾](Flutter-Interact-2019.md) - * [Flutter 升级 1.12 适配教程](Flutter-update-1.12.md) - * [Spuernova 是如何提升 Flutter 的生产力](Flutter-Supernova.md) - * [Flutter 中的图文混排与原理解析](Flutter-TWHP.md) - * [Flutter 实现视频全屏播放逻辑及解析](Flutter-Player-Full.md) - * [Flutter 上的一个 Bug 带你了解键盘与路由的另类知识点](Flutter-keyboard-rs.md) - * [Flutter 上默认的文本和字体知识点](Flutter-Font-Other.md) - * [带你深入理解 Flutter 中的字体“冷”知识](Flutter-Font-Cool.md) - * [Flutter 1.17 中的导航解密和性能提升](Flutter-nav+1_17.md) - * [Flutter 1.17 对列表图片的优化解析](Flutter-Image+1_17.md) - * [Flutter 1.20 下的 Hybrid Composition 深度解析](flutter-hy-composition.md) - * [2020 腾讯Techo Park - Flutter与大前端的革命](Flutter-TECHO.md) - * [带你全面了解 Flutter,它好在哪里?它的坑在哪里? 应该怎么学?](Flutter-WHAT.md) - * [Flutter 中键盘弹起时,Scaffold 发生了什么变化](Flutter-KEY.md) - * [Flutter 2.0 下混合开发浅析](Flutter-Group.md) - * [Flutter 搭建 iOS 命令行服务打包发布全保姆式流程](Flutter-iOS-Build.md) - * [不一样角度带你了解 Flutter 中的滑动列表实现](Flutter-N-Scroll.md) - * [带你深入 Dart 解析一个有趣的引用和编译实验](DEMO-INTEREST.md) - * [Dart 里的类型系统](Dart-SYS.md) - * [Dart VM 的相关简介与运行模式解析](Dart-VM.md) - * [Flutter 里的语法糖解析,知其所然方能潇洒舞剑](Flutter-SU.md) - * [Flutter 实现完美的双向聊天列表效果,滑动列表的知识点](Flutter-SC.md) - * [Flutter 启动页的前世今生适配历程](Flutter-LA.md) - * [Flutter 快速解析 TextField 的内部原理](Flutter-TE.md) - * [谷歌DevFest 2021 广州国际嘉年华-带你了解不一样的 Flutter](Flutter-DevFest2021.md) - * [Flutter for Web 2022 年:简单探讨](Flutter-W2022.md) - * [2021 年的 Flutter 状态管理:如何选择?](Flutter-StateM.md) - * [Flutter 2.10 升级填坑指南](Flutter-210-FIX.md) - * [Flutter Riverpod 全面深入解析,为什么官方推荐它?](Flutter-Riverpod.md) - * [ Flutter 2022 战略和路线解读与想法](Flutter-2022-roadmap.md) - * [原生开发如何学习 Flutter | 谷歌社区说](Flutter-SQS.md) - * [Fluttter 混合开发下 HybridComposition 和 VirtualDisplay 的实现与未来演进](Flutter-HV.md) - * [Flutter 双向聊天列表效果进阶优化](Flutter-Chat2.md) - * [Flutter 上字体的另类玩法:FontFeature ](Flutter-FontFeature.md) - +* [移动端系统生物认证技术详解](Flutter-BIO.md) +* [完整解析使用 Github Action 构建和发布 Flutter 应用](Flutter-GB.md) +* [Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结](Flutter-120HZ.md) +* [Flutter Festival | 2022 年 Flutter 适合我吗?Flutter VS Other 量化对比](Flutter-FF.md) +* [Flutter 从 TextField 安全泄漏问题深入探索文本输入流程](Flutter-TL.md) +* [Flutter iOS OC 混编 Swift 遭遇动态库和静态库问题填坑](Flutter-BIOS.md) * [Flutter Web : 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md) +* [大前端时代的乱流:带你了解最全面的 Flutter Web](Flutter-Web-T.md) +* [Flutter 深入探索混合开发的技术演进](Flutter-DWW.md) +* [Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer](Flutter-P3.md) +* [Google I/O Extended | Flutter 游戏和全平台正式版支持下 Flutter 的现状](Flutter-Extended.md) +* [掘金x得物公开课 - Flutter 3.0下的混合开发演进](Flutter-DWN.md) +* [Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty ](Flutter-N1.md) +* [Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3 ](Flutter-N2.md) +* [Flutter 小技巧之玩转字体渲染和问题修复 ](Flutter-N3.md) +* [Flutter 小技巧之有趣的动画技巧](Flutter-N4.md) +* [Flutter 小技巧之 Dart 里的 List 和 Iterable 你真的搞懂了吗?](Flutter-N6.md) - diff --git a/Flutter-120HZ.md b/Flutter-120HZ.md new file mode 100644 index 0000000..e73cff2 --- /dev/null +++ b/Flutter-120HZ.md @@ -0,0 +1,166 @@ +# Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结 + + +## 一、无用的知识 + +首先科普无用的知识,说起高刷新率,就不得不提两个词汇: **ProMotion** 和 **LTPO** 。 ProMotion 是 iOS 在支持 120hz 之后出现的动态刷新率支持,也就是不同场景使用不同的屏幕刷新率,从而实现体验上提升的同时降低了电池的消耗。 + +![c64c73ef829cb88f10f35ae24e5a6c59](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image1) + + + +LTPO(low-temperature Polycrystalline oxide) 允许显示器动态改变屏幕刷新率 ,而早在三星S20 Ultra、OPPO Find X3系列、一加 9 Pro 等系列产品上都率先采用了这种显示技术,但是实际上大家在 LTPO 又有不同的技术调教,从而出现了我们后续要聊的问题。 + +![image-20220331153929592](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image2) + +例如 LTPO 1.0 时代可能大部分实现都只是强硬的根据场景锁死 60Hz/120Hz 的刷新率,而 LTPO 2.0 开始各大厂家则是升级了自适应策略,例如最常见的就是升级了滑动变频: + +![0ecaee4af2444b87a73db171bd36ba3f](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image3) + +当然,除了最常见的滑动, LTPO 2.0 上厂家可能还会有对动画、视频、文字输入、应用切换等场景进行不同的升频和降频策略,而其实介绍上面这些的原因是: + +- **苹果 ProMotion 是基于官方实现的统一方案**; +- **Android 的 LTPO 是基于供应商硬件后Android OEM 厂家自主调教的实现**; + +> 以上部分资料来自[《LTPO到底是不是真的省电?-一加LTPO 2.0上手体验》](https://mobile.it168.com/a2022/0121/6612/000006612347.shtml) + +所以这也造就了 Flutter 需要在 Android 和 iOS 上进行单独适配的主要原因。 + +## 二、Android + +前面介绍里引用了一加的 LTPO 2.0 实现是有原因的,首先知道**自适应屏幕刷新率是 OEM 厂商自主调教,也就是理论上作为 App 是不需要做任何适配,因为跟随 Android 就行,Android 本身也是使用 Skia 渲染。** + +但是往往事与愿违,在 Flutter 关于 [高刷问题 ](https://github.com/flutter/flutter/issues/35162) 最先被提及的就是一加,那时候基本都引用了 [《The OnePlus 7 Pro’s 90Hz Refresh Rate Doesn’t Support Every App 》](https://www.xda-developers.com/oneplus-7-pro-true-90hz-display-mode/) 这篇文章: + +> 一加 7 Pro 的 90 fps 模式对于某些 App 而言只有 60 fps,要在所有 App 上都强制 90 fps,需要执行 `adb shell settings put global oneplus_screen_refresh_rate 0 ` 命令, 相比之下 Pixel 4 无需任何更改就直接可以支持渲染 90 fps 的 Flutter App。 + +也就是问题最开始是在一加的 90 fps 上不支持,而社区通过和一加的沟通得到的回复是: + +- 一加7 Pro 为了平衡性能和功耗,采用的是基于 Android 定制自己的帧率控制逻辑,一般屏幕会以高帧率工作,但在某些场景下系统会切回到低帧率,而由于引入了这种机制,可能会出现当 App 希望屏幕以高帧率运行时却被系统强制设置为低帧率的问题。 + +- 那如何通过 App 设置 fps ? **如果应用程序需要设置帧速率,那首先需要通过 `getSupportedModes()` 获取目前屏幕支持的模式列表,然后遍历列表,根据找到想要使用的分辨率和刷新率的 `modeId`,赋值给窗口的`preferredDisplayModeId`**。 + +所以基于这个问题修复的方案,社区内提出了 [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) 插件,插件主要提供了获取 `Display.Mode` 和设置 `preferredDisplayModeId` 的支持,用于临时解决类似 一加7 Pro 上的这种刷新率问题。 + +```dart +/// On OnePlus 7 Pro: +/// #1 1080x2340 @ 60Hz +/// #2 1080x2340 @ 90Hz +/// #3 1440x3120 @ 90Hz +/// #4 1440x3120 @ 60Hz +/// On OnePlus 8 Pro: +/// #1 1080x2376 @ 60Hz +/// #2 1440x3168 @ 120Hz +/// #3 1440x3168 @ 60Hz +/// #4 1080x2376 @ 120Hz +``` + +那什么是 `PreferredDisplayModeId` ?通过官方的 [《setframerate-vs-preferreddisplaymodeid》](https://developer.android.com/guide/topics/media/frame-rate#setframerate-vs-preferreddisplaymodeid) 可以了解: + +> `WindowManager.LayoutParams.preferredDisplayModeId` 是 App 向平台设置所需帧率的一种方式,因为有时候 App 只想改变刷新率,但是不需要更改其他显示模式如分辨率等。类似设置还有 `setFrameRate() ` ,使用 `setFrameRate()` 代替 `preferredDisplayModeId`会更简单, 因为`setFrameRate()` 可以自动匹配显示模式列表里具有特定帧速率的模式。 + +**那为什么不直接用 `setFrameRate` ?其中之一因为这是一个 Target 很高的 API**。 + +![image-20220331170424637](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image4) + +> PS:**这里和大家介绍一位 Flutter 大佬, 事实上这个 [问题](https://github.com/flutter/flutter/issues/93688) 作为 GDE 的 [AlexV525](https://github.com/AlexV525) 大佬跟进了很久,上面的插件也是他在参与维护,同时也恭喜🎉 大佬获得 [Google Open Source Peer Bonus Winners in 2022](https://opensource.googleblog.com/2022/03/Announcing-First-Group-of-Google-Open-Source-Peer-Bonus-Winners-in-2022.html) 的🏆**。 + +但是在安稳一段时间之后,[一加 9 pro 上了 LTPO 和 ColorOS](https://github.com/ajinasokan/flutter_displaymode/issues/10),之前的 adb 命令在新来的 ColorOS 上也随之失效,不过不要担心,后续发现这个其实是官方的一个bug,在 ColorOS `11_A.06` 版本后修复了该问题,也就是插件还可以继续生效。 + +而如今两年快过去了,对于此问题还是只能通过插件去临时解决,因为从官方的态度上好像并不是特别支持嵌入这种方式: + +- Flutter 应该将刷新率控制交给 OS 处理, Flutter 不应该对单个刷新率去进行 hardcode; +- 处理类似 OEM 厂商问题最好通过插件解决而不是 Flutter Engine ; + +> 在这方面的处理思路和决策感觉和 iOS 差异较大,大概也有平台限制的因素吧。 + +事实上不同厂商对于 LTPO 的实现逻辑确实差异性很大,比如下图是一加10pro 在 LTPO 渲染是会选择性压缩或者丢弃一些冗余的指令。 + +![8888](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image5) + +我们知道 Flutter 是把 ` Widget` 渲染到 `Surface` 上,在这点上和使用 ` SurfaceView` 和 `OpenGL` 实现的 Google Map 很类似,而经过测试 Google Map 在这些设备上,不特殊设置和 Flutter 一样也只能以 60hz 渲染运行。 + +> 对于 OEM 厂商,在调教的 LTPO 上有权决定是否允许 App 使用更高的刷新率,即使 App 要求更高的刷新率,这难道又是一个“白名单模式”? + +所以如果需要让 `Surface` 在某些特殊设备支持 90/120 hz 运行,就需要使用 `preferredDisplayModeId` 或者 `setFrameRate` , **同时前提是厂商没有强行锁死帧率**。 + +> **一些手机厂商,会因为 “驯龙” 和控温的需要,都有自己的“稳帧”策略,甚至强制锁死帧率并且显示假帧率**。 + +![22222](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image6) + +而在 [#78117](https://github.com/flutter/flutter/issues/78117) 讨论的最终讨论结果就是:**Flutter 并不会特别针对这部分厂商去特意做适配,如果需要,你可以通过第三方插件来解决,当然在我的测试中,目前大部分设备的刷新率支持上还是正常**。 + +同时在早期 Flutter 的 IntelliJ 插件也存在 bug ,即使应用程序以 90 fps 运行,Android Studio / IntelliJ 中的 Flutter 插件也会给出 60 fps ,当然这个问题在后续的 [#4289](https://github.com/flutter/flutter-intellij/pull/4289) 上得到了解决。 + +> 额外补充一种情况,厂家通常还会检测 `SurfaceView`/`TextureView ` 是否超过屏幕的一半,因为这时候可能代表着你正在看视频或者玩游戏,而这时候可能也会降低帧率。 + + + +最后,如果对 Flutter 在 Android 上关于刷新率部分的代码感性起,可以查阅:[vsync_waiter.cc](https://github.com/flutter/engine/blob/ebcd86f681b9421318b3b4a8abd75839e70000a5/shell/common/vsync_waiter.cc) 、[vsync_waiter_android.cc](https://github.com/flutter/engine/blob/266d3360a7babfb5f20d5e9f8ea84772b2a247dc/shell/platform/android/vsync_waiter_android.cc) 、[android_display.cc](https://github.com/flutter/engine/blob/266d3360a7babfb5f20d5e9f8ea84772b2a247dc/shell/platform/android/android_display.cc) + + + +## 三、iOS + +回到 iOS 上,ProMotion 的支持思路就和原生不大一样,因为在刚推出 ProMotion 时官方就在 [《刷新率优化上》](https://developer.apple.com/documentation/quartzcore/optimizing_promotion_refresh_rates_for_iphone_13_pro_and_ipad_pro) 对 ProMotion 的适配提及过: + +如果使用的是以下这些默认框架的话,对于这些刷新率的变化 App 而无需进行任何更改: + +- [UIKit](https://developer.apple.com/documentation/uikit) +- [SwiftUI](https://developer.apple.com/documentation/swiftui) +- [SpriteKit](https://developer.apple.com/documentation/spritekit) +- [CAAnimation](https://developer.apple.com/documentation/quartzcore/caanimation) + +但是对于 Flutter 而言并没用使用系统所提供的原生控件,所以目前需要在 `Info.plist` 文件中配置以下参数,从而启用关于 `CADisplayLink` 和 `CAAnimation` 上高于 120Hz 的相关支持: + +``` +CADisableMinimumFrameDurationOnPhone +``` + +而在 Flutter 官方的讨论记录 [flutter.dev/go/variable-refresh-rate](https://flutter.dev/go/variable-refresh-rate) 和 issue [#90675](https://github.com/flutter/flutter/issues/90675) 相关回复里可以看到,官方目前的决策是先使用 [#29797](https://github.com/flutter/engine/pull/29797) 的实现解决,通过调整 [vsync_waiter_ios.mm](https://github.com/flutter/engine/blob/4a3e7a5b72363c1f363d3000d04719c6938d963f/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm) 相关的内容来实现高刷支持: + +```objective-c +- (void)setMaxRefreshRateIfEnabled { + NSNumber* minimumFrameRateDisabled = + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]; + if (!minimumFrameRateDisabled) { + return; + } + double maxFrameRate = fmax([DisplayLinkManager displayRefreshRate], 60); + double minFrameRate = fmax(maxFrameRate / 2, 60); + + if (@available(iOS 15.0, *)) { + display_link_.get().preferredFrameRateRange = + CAFrameRateRangeMake(minFrameRate, maxFrameRate, maxFrameRate); + } else if (@available(iOS 10.0, *)) { + display_link_.get().preferredFramesPerSecond = maxFrameRate; + } +} + +``` + +- 默认情况下帧率会是设置为 60; +- 在支持 ProMotion 的设备上会设置为显示器支持的最大刷新率; +- **在 iOS 15 及更高版本上,还增加了设置帧率范围**,其中 preferred 和 max 均为屏幕支持的最大值,min 为最大值的 1/2; + +其实在之前的讨论中还有如 [#29692](https://github.com/flutter/engine/pull/29692) 这种更灵活的实现,**也就是探索让 Flutter Engine 根据渲染和使用场景去自己选择当前的帧率**,因为社区认为:*对于普通用户来说,在不知道平台、性能等的情况下让开发者自己选择正确的刷新并不靠谱,所以通过 Engine 完成适配才是未来的方向*。 + +**当然,基于社区里目前迫切地想让 Flutter 得到 120Hz 的能力,所以会暂时优先采用上述的 `CADisableMinimumFrameDurationOnPhone` 来解决目前的困境,这也是 iOS 官方提倡的方式**。 + +另外值得一提的是,iOS 15.4 上的苹果修复了导致 ProMotion 相关的 bug ,因为在这之前会出现 ProMontion 并不是完全开放第三方支持的诡异情况,**而在 iOS 15.4 后, iOS 会自动为 App 中所有自定义动画内容启用120Hz刷新率**,所以会出现一个神奇的情况: + +- 在 iOS 15.4 上, App 可以兼容得到 120Hz 动画; +- 在 iOS 15.4 之前,部分动画支持 ProMotion; + +![image-20220331182557253](http://img.cdn.guoshuyu.cn/20220627_Flutter-120HZ/image7) + + + +## 四、最后 + +可以看到就目前来说,高刷对于 Flutter 仍旧是一个挑战,作为独立渲染引擎,这也是 Flutter 无法逃避的问题,就目前情况来看: + +- 在 Android 上你不需要做任何调整,如果遇到特殊设备或者系统,建议通过 [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) 来解决; +- 在 iOS 上你可以添加 `CADisableMinimumFrameDurationOnPhone` 来粗暴解决,然后等待 [#29797](https://github.com/flutter/engine/pull/29797) 相关内容的合并发布; + +最后,如果关于高刷方面你还有什么资料或者想法,欢迎留言评论讨论。 \ No newline at end of file diff --git a/Flutter-300.md b/Flutter-300.md new file mode 100644 index 0000000..b7dd823 --- /dev/null +++ b/Flutter-300.md @@ -0,0 +1,313 @@ +# Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O + +> 本次 Flutter 3.0 主要包括 macOS 和 Linux 的稳定版发布,以及相关的性能改进等。原文链接 https://medium.com/flutter/whats-new-in-flutter-3-8c74a5bc32d0 + + +又到了发布 Flutter 稳定版本的时候,在三个月前我们发布了 Flutter 关于 Windows 的稳定版,而今天,除 Windows 之外,**Flutter 也正式支持 macOS 和 Linux 上的稳定运行**。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image1) + +在这里感谢所有 Flutter contributors 的辛勤工作,本次版本发布合并了 5248 个 PR。 + +**Flutter 3.0 的发布,主要包括 Flutter 对 macOS 和 Linux 正式版支持、进一步的性能改进、手机端和 Web 端相关的更新等等。此外还有关于减少对旧版本 Windows 的支持,以及一些 breaking changes 列表**。 + +# 稳定版 Flutter 已经支持所有桌面平台 + +Linux 和 macOS 已达进入稳定版本阶段,包括以下功能: + +## 级联菜单和对 macOS 系统菜单栏的支持 + +现在可以使用 `PlatformMenuBar` 在 macOS 上创建菜单栏,该 Widget 支持仅插入平台菜单,并控制 macOS 菜单中显示的内容。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image2) + +## 所有桌面平台全面支持国际化文本输入 + +包括使用[input method editors](https://en.wikipedia.org/wiki/Input_method)(IME),如中文、日文和韩文,在 Flutter 3.0 上所有桌面平台上都得到支持,包括第三方输入法如搜狗和谷歌日文输入法。 + +## 所有桌面平台的 Accessibility + +Flutter for Windows、macOS 和 Linux 全面支持 Accessibility 服务,例如屏幕阅读、无障碍导航和倒置颜色等。 + +## macOS 上默认的 Universal binaries + +从 Flutter 3 开始,Flutter macOS 桌面应用都将被构建为 universal binaries,从而支持现有的基于 Intel 处理器的 Mac, 和 Apple 的 Apple Silicon 设备。 + + +## 放弃 Windows 7/8 + +在 Flutter 3.0 中,推荐将 Windows 的版本提升到 Windows 10,虽然目前 Flutter 团队不会阻止在旧版本(Windows 7、Windows 8、Windows 8.1)上进行开发,但 [Microsoft 不再支持](https://docs.microsoft.com/en-us/lifecycle/faq/windows) 这些版本,虽然 Flutter 团队将继续为旧版本提供“尽力而为”的支持,但还是鼓励开发者升级。 + +> **注意**:目前还会继续为在 Windows 7 和 Windows 8 上能够正常*运行* Flutter 提供支持;此更改仅影响开发环境。 + +# 移动端更新 + +对移动端的更新包括以下内容: + +## 折叠手机的支持 + +Flutter 3 版本开始支持可折叠的移动设备。在 Microsoft 发起的合作中,新功能和 Widget 可让开发者在可折叠设备上拥有更舒适的体验。 + +**其中包括 `MediaQuery` 现在包含一个 `DisplayFeatures` 列表,用于描述设备的边界和状态**,如铰链、折叠和切口等。此外 `DisplayFeatureSubScreen` 现在可以通过定位其子 Widget 的位置不会与 `DisplayFeatures` 的边界重叠,并且目前已经与 framework 的默认对话框和弹出窗口集成,使得 Flutter 能够立即感知和响应这些**元素**。 + +![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image3) + +这里非常感谢 Microsoft 团队,尤其是[@andreidiaconu](https://github.com/andreidiaconu),感谢他们的 contributions!另外可以试用一下[Surface Duo 模拟器示例](https://docs.microsoft.com/en-us/dual-screen/flutter/samples),它包括一个带有 Flutter Gallery 特殊分支的示例,可以用于了解 Flutter 在折叠屏中的实际应用。 + +## iOS 可变刷新率支持 + +**Flutter 现在支持 iOS 上的 ProMotion 刷新率,包括 iPhone 13 Pro 和 iPad Pro 等**。 + +在这些设备上,Flutter 可以以达到 120 hz的刷新率进行渲染,再次之前 iOS 上的刷新率限制为 60hz,有关更多详细信息,请参阅[flutter.dev/go/variable-refresh-rate](http://flutter.dev/go/variable-refresh-rate)。 + +> 更多可见:[《Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结》](https://juejin.cn/post/7081273509690736653) + +## 简化 iOS 的发布 + +Flutter 团队 [为 flutter build ipa 命令添加了新选项](https://github.com/flutter/flutter/pull/97672)支持以简化发布 iOS 应用。 + +当开发者准备好分发到 TestFlight 或 App Store 时,可以通过运行 `flutter build ipa` 以构建 Xcode 存档(`.xcarchive`文件)和应用程序包(`.ipa`文件)。 这时候可以选择添加 `—-export-method ad-hoc`、` —-export-method development` 或 `—-export-method enterprise` 来定制发布支持。 + +[构建应用程序包后,可以通过 Apple Transport macOS 应用](https://apps.apple.com/us/app/transporter/id1450874784)或在命令行上使用 `xcrun altool`(运行 `man altool` 用于 App Store Connect API 的密钥身份验证)将其上传到 Apple 。上传后,应用就可以可发布到[TestFlight 或 App Store](https://docs.flutter.dev/deployment/ios#release-your-app-to-the-app-store)。 + +**通过这个简化流程,在设置初始的[Xcode 项目设置后](https://docs.flutter.dev/deployment/ios#review-xcode-project-settings),例如名称和应用图标,开发者可以不再需要打开 Xcode 来发布 iOS 应用**。 + +## Gradle 版本更新 + +现在使用 Flutter 工具创建新项目,会发现生现在开始使用最新版本的 Gradle 和 Android Gradle Plugin,对于现有项目,需要手动将版本升级到 Gradle 的 7.4 和 Android Gradle 插件的 7.1.2。 + +## 停用 32 位 iOS/iOS 9/iOS 10 + +正如 2022 年 2 月发布的 2.10 稳定版本时所说的那样,Flutter 对 32 位 iOS 设备以及 iOS 9 和 10 版本的支持即将结束。此更改影响 iPhone 4S、iPhone 5、iPhone 5C 以及第 2、3 和 4 代 iPad 设备。Flutter 3 是它们最后一个支持 iOS 版本支持。 + +> 要了解有关此更改的更多信息,请查看[RFC:End of support for 32-bit iOS devices](http://flutter.dev/go/rfc-32-bit-ios-unsupported)。 + + +# Web 更新 + +Web 应用更新包括以下内容: + +## 图像解码 + +Flutter web 现在会在支持它的浏览器中自动检测和使用 ImageDecoder API,而截至今天大多数基于 Chromium 的浏览器(Chrome、Edge、Opera、三星浏览器等)都添加了此 API。 + +**新的 API 使用浏览器的内置图像编解码器从主线程异步解码图像,这将图像解码速度提高了 2 倍,并且它从不阻塞主线程,从而消除了以前由图像引起的所有卡顿问题**。 + +## Web 应用的生命周期 + +Flutter Web 应用程序的新生命周期 API 使开发者可以更灵活地从托管 HTML 页面控制 Flutter 应用的引导过程,并帮助 Lighthouse 分析应用的性能,包括以下经常请求的场景: + +- 启动画面。 +- 加载指示器。 +- 在 Flutter 应用程序之前显示的纯 HTML 交互式登录页面。 + +> 有关更多信息,请查看docs.flutter.dev 上的[自定义 Web 应用程序初始化](https://docs.flutter.dev/development/platform-integration/web/initialization)。 + +# 工具更新 + +Flutter 和 Dart 工具的更新包括: + +## 更新的 lint 包 + +lint 包的 2.0 版已发布: + +- Flutter:[https ://pub.dev/packages/flutter_lints/versions/2.0.0](https://pub.dev/packages/flutter_lints/versions/2.0.0) +- Dart:[https ://pub.dev/packages/lints/versions/2.0.0](https://pub.dev/packages/lints/versions/2.0.0) + +**在 Flutter 3 中生成的应用程序会通过 `flutter create` 自动启用 v2.0 的 lints 集**。Flutter 现在鼓励现有的应用、包和插件都迁移到 v2.0 以遵循该协议,迁移支持可以通过运行 `flutter pub upgrade --major-versions flutter_lints`. + +**v2 中大多数新添加的 lint 警告都带有自动修复功能**。因此在 `pubspec.yaml` 文件中升级到最新的包版本后,可以运行 `dart fix —-apply` 自动修复大多数 lint 警告(可能一些警告仍然需要一些手动工作。 + +> 尚未使用 `package:flutter_lints` 的应用、软件包或插件可以按照[迁移指南](https://docs.flutter.dev/release/breaking-changes/flutter-lints-package#migration-guide)进行迁移。 + +## 性能改进 + +感谢 contributor [knopp](https://github.com/knopp),[局部重绘](https://github.com/flutter/engine/pull/29591)的支持已在 Android 设备上启用。 + +在本地测试中,此更改将Pixel 4 XL 设备在 `backdrop_filter_perf` 基准测试上, 90th percentile 和 99th 的帧光栅化时间减少了 5 倍,**现在在 iOS 和基础此更新的 Android 设备上都启用了,当存在单个矩形脏区域时的部分重绘支持**。 + +另外,Flutter 3.0 还进一步改进了[不透明动画相关的性能](https://github.com/flutter/engine/pull/30957),特别是当一个 `Opacity` Widget 只包含一个渲染 primitive 时, `Opacity` 下关于 `saveLayer` 的调用通常会被省略。在基准测试下中,这种情况下的光栅化时间提高了[一个数量级](https://flutter-flutter-perf.skia.org/e/?begin=1643063115&end=1644004520&keys=X32827d8819e8271e025f50e77bf2bec0&requestType=0&xbaroffset=27447),在未来的版本中,我们计划将此优化应用于更多场景。 + +再次感谢 contributor [JsouLiang](https://github.com/JsouLiang) 的提交,现在引擎的光栅和 UI 线程在 Android 和 iOS 上运行的优先级高于其他线程,例如 Dart VM 后台垃圾回收线程,而在我们的基准测试中,这导致平均框架构建时间[加快了约 20%](https://flutter-flutter-perf.skia.org/e/?begin=1644581114&end=1644647407&keys=X3999dc0a0c89054eaa9f66bcff27d882&num_commits=50&request_type=1&xbaroffset=27549)。 + +在 Flutter 3.0 之前,光栅缓存的准入策略仅查看图片中绘制操作的数量,不幸的是这会导致引擎花费更多的内存,来缓存实际上渲染速度非常快的图片。新版本[引入了一种机制](https://github.com/flutter/engine/pull/31417),该机制会根据其图片绘制操作的成本来估计图片的渲染复杂性,将其用作光栅缓存准入策略从而[减少内存使用量](https://flutter-flutter-perf.skia.org/e/?begin=1644790212&end=1646044276&keys=X4c7dd4e4903a38523816c00b31d4d787&requestType=0&xbaroffset=27636),并且不会在我们的基准测试中降低性能。 + +感谢 contributor [ColdPaleLight](https://github.com/ColdPaleLight),他修复了[帧调度](https://github.com/flutter/engine/pull/31513) 中的一个错误,该错误导致 iOS 上的少量动画帧被丢弃的问题。 + +## Impeller + +团队一直在努力寻找解决 iOS 和其他平台上卡顿的解决方案。**在 Flutter 3 版本中可以在 iOS 上preview 一个名为[Impeller](https://github.com/flutter/engine/tree/main/impeller) 的实验性渲染工具,Impeller 在引擎构建时会预编译[一组更小、更简单的着色器](https://github.com/flutter/flutter/issues/77412),这样它们就不会在应用程序运行时编译,这一直是 Flutter 中卡顿的主要来源**。 + +Impeller 尚未准备好正式发布,目前还远未到完成阶段,所以并非所有 Flutter 功能都能实现,但我们对它在 Flutter [/gallery](https://github.com/flutter/gallery) 应用程序中的保真度和性能感到非常满意,特别是 Gallery 应用里过渡动画中最差的帧快了大约 [20 倍](https://flutter-flutter-perf.skia.org/e/?begin=1650297849&end=1651261748&queries=sub_result%3Dworst_frame_rasterizer_time_millis%26test%3Dnew_gallery_impeller_ios__transition_perf%26test%3Dnew_gallery_ios__transition_perf&requestType=0)。 + +**Impeller 可以在 iOS 上通过启动 tag 来启动,开发者可以传递 `—-enable-impeller` 到`flutter run` 或将 `Info.plist` 文件中的 `FLTEnableImpeller` 标志设置为 `true` 来尝试 Impeller**。 + + +## Android上的内嵌广告 + +使用 `google_mobile_ads` 时,开发者应该会在用户关键交互(例如页面之间的滚动和转换)中得到更好的性能。 + +在底层,Flutter 现在使用新的异步组合来实现 Android 视图,它们通常称为[platform views](https://docs.flutter.dev/development/platform-integration/platform-views)。这意味着 Flutter 光栅线程不再需要等待 Android 视图渲染。相反,Flutter 引擎会使用它管理的 OpenGL 纹理将视图放置在屏幕上。 + +# 更多更新 + +Flutter 生态系统的其他更新包括: + +## Material 3 + +Flutter 3 支持[Material Design 3](https://m3.material.io/),即下一代 Material Design。 + +Flutter 3 为 Material 3 提供了更多可选支持,包括 Material You 功能如:**动态颜色,新的颜色系统和排版、组件的更新以及 Android 12 中引入的新视觉效果,如新的触摸波纹设计和拉伸过度滚动效果**。 + +>开发者可以在 codelab 的 [Take your Flutter app from Boring to Beautiful](https://codelabs.developers.google.com/codelabs/flutter-boring-to-beautiful) 中尝试 Material 3 功能,有关如何选择加入这些新功能,以及哪些组件支持 Material 3 的详细信息,请参阅[API 文档](https://api.flutter.dev/flutter/material/ThemeData/useMaterial3.html)。 + +## 主题扩展 + +Flutter 现在可以使用名为 *Theme extensions* 的概念向 Material 的 `ThemeData` 添加任何内容,开发者可以通过 `ThemeData`.extensions 去添加自己想要的内容,而不是(在 Dart 意义上)继承 `ThemeData` 并重新实现其`copyWith`、`lerp`和其他方法。 + +此外,作为 package 开发人员,你可以提供 `ThemeExtensions` 相关内容,有关此内容的更多详细信息,请参阅[flutter.dev/go/theme-extensions并](https://flutter.dev/go/custom-colors-m3) 和 GitHub 上 的[示例](https://github.com/guidezpl/flutter/blob/master/examples/api/lib/material/theme/theme_extension.1.dart)。 + +## Ads + +对于发布商而言,个性化广告征求同意并处理 Apple 的 App Tracking Transparency (ATT) 非常重要。 + +为了支持这些要求,Google 提供了用户消息传递平台 (UMP) SDK,它取代了之前的开源 [Consent SDK](https://github.com/googleads/googleads-consent-sdk-ios),在即将发布的 GMA SDK for Flutter 中,我们将添加对 UMP SDK 的支持,以帮助发布者获得用户同意。 + +> 有关更多详细信息,请查看 pub.dev 上的[google_mobile_ads](https://pub.dev/packages/google_mobile_ads)页面。 + +# Breaking changes + +随着 Flutter 的不断改进 ,我们的目标是尽量减少重大更改的数量,而随着 Flutter 3 的发布,Flutter 有以下重大变化: + +- [在 v2.10 之后删除了已弃用的 API](https://docs.flutter.dev/release/breaking-changes/2-10-deprecations) +- [由 ZoomPageTransitionsBuilder 替换的页面过渡](https://docs.flutter.dev/release/breaking-changes/page-transition-replaced-by-ZoomPageTransitionBuilder) +- [迁移 useDeleteButtonTooltip 到 Chips 的 deleteButtonTooltipMessage](https://docs.flutter.dev/release/breaking-changes/chip-usedeletebuttontooltip-migration) +- [ThemeData 的 toggleableActiveColor 属性已被弃用](https://docs.flutter.dev/release/breaking-changes/toggleable-active-color) + +> 如果你正在使用这些 API,请参阅[Flutter.dev 上的迁移指南](https://docs.flutter.dev/release/breaking-changes)。 + + + +# Flutter 3 相关介绍,包括Flutter桌面端、Flutter firebase 、Flutter游戏- 谷歌2022 I/O 大会, + +> 原本链接 https://medium.com/flutter/introducing-flutter-3-5eb69151622f + + +Flutter 3 作为 Google I/O 主题演讲的主要部分,Flutter 3 完成了 Flutter 从以移动为中心到多平台框架的路线图,本次提供了 **macOS 和 Linux 桌面应用相关的支持,以及对 Firebase 集成的改进、提高生产力和性能以及对 Apple Silicon 的支持等等**。 + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image4) + +# Flutter 3 之旅 + + Flutter 为了彻底改变应用的开发方式:将 Web 的迭代开发模型与以前游戏保留的硬件加速图形渲染和像素级控制相结合。 + +自 Flutter 1.0 beta 发布以来的过去四年里,Flutter 团队逐渐在这些基础上进行构建,添加了新的 framework 功能和新的 Widget,与底层平台更深入地集成,还有丰富的packages 支持以及许多性能和工具改进。 + +![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image5) + +随着产品的成熟,越来越多的人开始使用 Flutter 构建应用程序。如今有超过 500,000 个使用 Flutter 构建的应用程序。 + +来自 [data.ai](https://www.data.ai/en/)等研究公司的分析以及公开推荐表明,Flutter 被许多领域的[客户](https://flutter.dev/showcase)使用: + +- [微信等社交应用](https://play.google.com/store/apps/details?id=com.tencent.mm&hl=en_US&gl=US) +- [Betterment](https://apps.apple.com/us/app/betterment-investing-saving/id393156562)和[Nubank](https://play.google.com/store/apps/details?id=com.nu.production&hl=en_US&gl=US)等金融和银行应用; + +- [SHEIN](https://play.google.com/store/apps/details?id=com.zzkko&hl=en_US&gl=US)和[trip.com](https://apps.apple.com/us/app/trip-com-hotels-flights-trains/id681752345)等商务应用; +- [Fastic](https://fastic.com/)和[Tabcorp](https://auspreneur.com.au/tabcorp-adopts-googles-flutter-platform/)等生活方式应用; +- [My BMW](https://www.press.bmwgroup.com/global/article/detail/T0328610EN/the-my-bmw-app:-new-features-and-tech-insights-for-march-2021?language=en)等配套应用 +- [巴西政府](https://apps.apple.com/app/id1506827551)等公共机构; + +> **如今,有超过 500,000 个使用 Flutter 构建的应用程序**。 + +开发人员告诉我们,Flutter 可以更快地为更多平台构建精美的应用。在我们最近的用户研究中: + +- 91% 的开发人员同意 Flutter 减少了构建和发布应用所需的时间。 +- 85% 的开发者同意 Flutter 让他们的应用比以前更漂亮。 +- 85% 的人同意 Flutter 让他们能够为比以前更方便地在更多的平台发布他们的应用。 + +在 [Sonos 最近的一篇博客文章中](https://tech-blog.sonos.com/posts/renovating-setup-with-flutter/),他讨论了他们关于体验方便的改进,强调了其中的第二点: + +> “毫不夸张地说,解锁 [Flutter] 是有一定程度的‘*溢价*’,这与我们团队之前交付的任何东西都不同。对我们的设计师来说最重要的是,Flutter 可以轻松地构建新的 UI,这意味着我们的团可以花更少的时间对规范说“不”,而将更多的时间用于迭代规范。这听起来值得,所以我们建议大家可以尝试一下 Flutter。” + + +# Flutter 3 介绍 + +**借助 Flutter 3,开发者可以通过一个代码库为六个平台构建应用**,为开发人员提供无与伦比的生产力,并帮助初创公司在一开始就将新想法快速得带入完整的目标市场。 + +在之前的版本中,我们在 iOS 和 Android 的技术上添加了[Web](https://medium.com/flutter/flutter-web-support-hits-the-stable-milestone-d6b84e83b425) 和 [Windows 支持](https://medium.com/flutter/announcing-flutter-for-windows-6979d0d01fed),现在**Flutter 3 增加了对 macOS 和 Linux 应用程序的稳定支持**。 + +添加对应平台的支持不仅仅是渲染像素:**它包括新的输入和交互模型、编译和构建支持、accessibility 和国际化以及特定于平台的集成等等,Flutter 团队的目标是让开发者能够灵活地利用底层操作系统,同时根据开发者的选择尽可能多的共享 UI 和逻辑**。 + +在 macOS 上,现在支持 Intel 和 Apple Silicon,提供[Universal Binary](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary)的支持,允许应用打包支持两种架构上的可执行文件。在 Linux 上,Canonical 和 Google 合作提供了一个最佳的开发选项。 + +[Superlist](https://superlist.com/)是 Flutter 如何实现 Desktop 应用的一个很好的例子,它会今天在测试版中发布。 + +Superlist 通过将列表、任务和自由格式内容组合成全新的待办事项列表和个人计划的新应用程序,提供协作能力,而 Superlist 团队之所以选择 Flutter,是因为它能够提供快速、高度品牌化的桌面体验,我们认为他们迄今为止的进步证明了为什么 Flutter 是一个不错的选择。 + + +Flutter 3 还改进了许多基础功能,包括了改性能、Material You 支持和开发效率的提高。 + +除了上面提到的工作,在这个版本中,Flutter 现在支持完全给予原生[Apple 芯片](https://support.apple.com/en-us/HT211814)进行开发,虽然 Flutter 自发布以来一直与基于 M1 的 Apple 设备兼容,但 Flutter 现在可以充分利用了[Dart 对 Apple 芯片的支持](https://medium.com/dartlang/announcing-dart-2-14-b48b9bb2fb67),从而能够在基于 M1 的设备上更快地编译并支持 macOS 应用程序的 [Universal Binary](https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary) 文件 + +我们对[Material Design 3](https://m3.material.io/)的工作也在此版本中基本完成,它允许开发人员提供动态配色方案和新的视觉组件,以适应性强的跨平台设计系统: + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image6) + + +Flutter 由 Dart 提供支持,Dart 是一种用于多平台开发的高生产力、可移植语言,我们在这个周期中对 Dart 的改进工作包括有: +- 减少样板文件; +- 提高可读性的新语言功能; +- 实验性 RISC-V 支持; +- 升级的 linter 和新文档; + +>有关 Dart 2.17 中所有新改进的更多详细信息,请查看[博客](https://medium.com/dartlang)。 + + +# Firebase 和 Flutter + +当然,构建应用的不仅仅是 UI , 应用的发布者需要一整套工具来构建、发布和运行应用,包括: 身份验证、数据存储、云功能和设备测试等服务。 + +目前有多种服务都已经支持 Flutter,包括[Sentry](https://docs.sentry.io/platforms/flutter/)、[AppWrite](https://appwrite.io/docs/getting-started-for-flutter)和 [AWS Amplify](https://docs.amplify.aws/start/q/integration/flutter/)。 + +Google 提供的应用服务是 Firebase,[SlashData ](https://www.slashdata.co/developer-program-benchmarking/?)的开发者基准测试研究表明,62% 的 Flutter 开发者在他们的应用中使用 Firebase。 + +因此,在过去的几个版本中,我们一直在与 Firebase 合作,以便能更好地将 Flutter 的集成。这包括将 Flutter 的 Firebase 插件发布到 1.0,添加更好的文档和工具,以及[FlutterFire UI](https://pub.dev/packages/flutterfire_ui)等新 Widget,为开发人员提供可重用的身份验证和配置文件界面 UI 等等。 + +而在今天,我们宣布将 Flutter/Firebase 集成升级为 Firebase 产品的核心支持。我们正在将源代码和文档转移到 Firebase 存储库和站点中,开发者可以期待我们与 Android 和 iOS 同步发展 Firebase 对 Flutter 的支持。 + +此外,我们还进行了一些重大改进,以支持使用 Firebase 时支持崩溃报告服务 Crashlytics。通过Flutter [Crashlytics 插件](https://firebase.google.com/docs/crashlytics),开发者可以实时跟踪致命错误,提供与 iOS 和 Android 开发人员相同的功能集。 + +这包括重要的警报和指标,如“无崩溃用户”可帮助开发者掌握应用的稳定性。Crashlytics 分析管道已升级和改进对 Flutter 崩溃的支持,从而更快可以地对问题进行分类、优先排序和修复问题。 + +最后我们简化了插件设置过程,因此只需几个步骤即可从 Dart 代码中启动和运行 Crashlytics。 + + + +# Flutter 休闲游戏工具包 + +对于大多数开发者来说,Flutter 是一个应用框架。但是随着休闲游戏开发社区也在不断壮大,利用 Flutter 提供的硬件加速图形支持以及[Flame](https://flame-engine.org/)等开源游戏引擎的需求一致在提高。 + +我们想让休闲游戏开发者更容易上手,因此在今天的 I/O 上,我们宣布发布[休闲游戏工具包](https://flutter.dev/games),它提供的模板和最佳实践的入门工具包以及广告和云服务。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image7) + +尽管 Flutter 并非专为高强度 3D 动作游戏而设计的渲染引擎,但其中一些游戏的非游戏 UI 已经开始转向 Flutter ,包括拥有数亿用户的热门游戏,如[PUBG Mobile ](https://play.google.com/store/apps/details?id=com.tencent.ig)。 + +对于 I/O,我们想看看我们可以将这项技术推到多远,所以 Flutter 团队创建了一个有趣的弹球游戏,由 Firebase 和 Flutter 的网络支持提供支持。 + +[I/O Pinball](https://pinball.flutter.dev/ ) 提供了一个围绕 Google 的吉祥物设计的游戏:Flutter 的 Dash、Firebase 的 Sparky、Android 机器人和 Chrome 恐龙,我们认为这是展示 Flutter 的一种有趣方式。 + + +![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-300/image8) + +# 由 Google 赞助,由社区提供支持 + +我们喜欢 Flutter 的原因,不仅仅是一款 Google 开发的产品——而是因为它是一款“所有人”的产品。 + +开源意味着我们都可以参与并受益于它的成功,无论是通过贡献新代码或文档,创建核心框架软件包,编写书籍和培训课程来教授他人。 + +为了展示社区的最佳状态,我们最近与 DevPost 合作赞助了 Puzzle Hack 挑战赛,让开发人员有机会通过使用 Flutter 重新构想经典的滑动拼图来展示他们的技能,这将展示 web, desktop 和 mobile如何结合。 + +> 相关的视频链接:https://youtu.be/l6hw4o6_Wcs \ No newline at end of file diff --git a/Flutter-BIO.md b/Flutter-BIO.md new file mode 100644 index 0000000..cb1aa50 --- /dev/null +++ b/Flutter-BIO.md @@ -0,0 +1,272 @@ +# 移动端系统生物认证技术详解 + + +相信大家对于生物认证应该不会陌生,使用指纹登陆或者 FaceId 支付等的需求场景如今已经很普遍,所以基本上只要涉及移动端开发,不管是 Android 、iOS 或者是 RN 、Flutter 都多多少少会接触到这一业务场景。 + +当然,不同之处可能在于大家对于平台能力或者接口能力的熟悉程度,**所以本篇主要介绍 Android 和 iOS 上使用系统的生物认证需要注意什么,具体流程是什么,给需要或者即将需要的大家出一份汇总的资料**。 + +> ⚠️注意:**本篇更倾向于调研资料的角度,适合需要接入或者在接入过程中出现疑问的方向,而不是 API 使用教程,另外篇幅较长警告~** + +首先,先简单说一个大家都知道的概念,那就是不管是 Android 或者 iOS ,不管是指纹还是 FaceId ,只要使用的是系统提供的 API ,**作为开发者是拿不到任何用户的生物特征数据,所以简单来说你只能调用系统 API ,然后得到成功或者失败的结果**。 + + + +![image-20220329172335926](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image1) + + + +## 一、Android + +Android 上的生物认证发展史可以说是十分崎岖,目前简单来说经历了两个阶段: + +- `FingerprintManager` (API 23) +- `BiometricPrompt`(API 28) + +所以如下图所示,你会看到其实底层有两套 `Service` 在支持生物认证的 API 能力,但是值得注意的是, **`FingerprintManager ` 在 Api28(Android P)被添加了 `@Deprecated` 标记 ,包括 androidx 里的兼容包 `FingerprintManagerCompat` 也是被标注了 `@Deprecated` ,因为官方提供更傻瓜式,更开箱即用的 `androidx.biometrics.BiometricPrompt`**。 + + + + + +![image-20220329172806492](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image2) + + + +### 1.1、使用 BiometricPrompt + +简单介绍下接入 `BiometricPrompt` ,首先第一步是添加权限 + +```xml + + + + + + +``` + +接着调用 `BiometricPrompt` 构建系统弹出框信息,具体内容对应可见下图: + +![image-20220329175140111](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image3) + +最用设置 `AuthenticationCallback` 和调用 `authenticate` ,然后等待授权结果进入到成功的回调: + +```java + biometricPrompt = new BiometricPrompt(activity, uiThreadExecutor, authenticationCallback); + biometricPrompt.authenticate(promptInfo); +``` + +当然上述代码还少了很多细节: + +- 比如需要是 `FragmentActivity` ; + +- 检测设备是否支持生物认证(还有不支持的现在?); +- 判断支持哪种生物认证,当然默认 `BiometricPrompt` 会帮你处理,如果有多种会弹出选择; + +而认证不成功的时候可以在 `onAuthenticationError` 里获取到对应的错误码: + +| onAuthenticationError | Type | +| --------------------------------- | ------------------------------------------------------------ | +| BIOMETRIC_ERROR_LOCKOUT | 操作被取消,因为 API 由于尝试次数过多而被锁定(一般就是在一次 `authenticate` 里例如多次指纹没通过,锁定了, 但是过一会还可以调用) | +| BIOMETRIC_ERROR_LOCKOUT_PERMANENT | 由于 BIOMETRIC_ERROR_LOCKOUT 发生太多次,操作被取消,这个就是真的 LOCK 了。 | +| BIOMETRIC_ERROR_NO_SPACE | 剩余存储空间不足 | +| BIOMETRIC_ERROR_TIMEOUT | 超时 | +| BIOMETRIC_ERROR_UNABLE_TO_PROCESS | 传感器异常或者无法处理当前信息 | +| BIOMETRIC_ERROR_USER_CANCELED | 用户取消了操作 | +| BIOMETRIC_ERROR_NO_BIOMETRIC | 用户没有在设备中注册任何生物特征 | +| BIOMETRIC_ERROR_CANCELED | 由于生物传感器不可用,操作被取消 | +| BIOMETRIC_ERROR_HW_NOT_PRESENT | 设备没有生物识别传感器 | +| BIOMETRIC_ERROR_HW_UNAVAILABLE | 设备硬件不可用 | +| BIOMETRIC_ERROR_VENDOR | 如果存在不属于上述之外的情况,Other | + +### 1.2、BiometricPrompt 自定义 + +简单接入完 `BiometricPrompt` 之后, 你可能会有个疑问: *`BiometricPrompt` 是很方便,但是 UI 有点丑了,可以自定义吗?* + +**抱歉,不可以** ,是的,`BiometricPrompt `不能自定义 UI,甚至你想改个颜色都“费劲”, 如果你去看 [biometric](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/biometric) 的源码,就会发现官方并没有让你自定义的打算,除非你 cv 这些代码自己构建一套,至于为什么会有这样的设计,我个人猜测**其中一条就是屏下指纹**。 + +> 在官方的 [《Migrating from FingerprintManager to BiometricPrompt》](https://medium.com/androiddevelopers/migrating-from-fingerprintmanager-to-biometricprompt-4bc5f570dccd)里也说了:丢弃指纹的布局文件,因为你将不再需要它们,AndroidX 生物识别库带有标准化的 UI。 + +什么是标准化的 UI ?如下所示是使用 `BiometricPrompt` 的三台手机,可以看到: + +- 第一和第二台除了位置有些许不同,其他基本一致; +- 第三胎手机是屏下指纹,可以看到整个指纹输入的 UI 效果完全是厂家自己的另外一种风格; + +![image-20220329183129880](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image4) + +**所以使用 `BiometricPrompt` 你将不需要关注 UI 问题,因为你没得选,甚至你也不需要关注手机上的生物认证类型的安全度问题,因为不管是 CDD 还是 UI ,OEM 厂商的都会直接实现好**,例如三星的 UI 是如下图所示: + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image5) + + + +> [Android 兼容性定义文档 (Android CDD)](https://source.android.com/compatibility/android-cdd#7_3_10_biometric_sensors)_里描述了生物认证传感器安全度的强弱,而在 framework 层面 `BiometricFragment` 和 `FingerprintDialogFragment` 都是 `@hide` ,甚至你单纯去翻 `androidx.biometric:biometric.aar` 的库,你都看不到 `BiometricFragment` 的布局,只能看到 `FingerprintDialogFragment` 的 layout。 + +那就没办法自定义 UI 了吗?还是有的,有两个选择: + +- 继续使用 `FingerprintManager` ,虽然标注了弃用,但是目前还是可以用,在 Android 11 上也可以正常执行对应逻辑,下图是同一台手机在 Android 11 上使用 `FingerprintManager` 和 `BiometricPrompt ` 的对比: + + ![image-20220329202726403](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image6) + +- 使用腾讯的 [soter ](https://github.com/Tencent/soter ) ,这个我们后面讲; + + + +### 1.3、Login + BiometricPrompt + +介绍完调用和 UI ,那就再结合 Login 场景聊聊 `BiometricPrompt` ,官方针对 Login 场景提供了一个 [Demo]( https://github.com/android/security-samples/tree/master/BiometricLoginKotlin) ,这里主要介绍整个业务流程,具体代码可以看官方的 [BiometricLoginKotlin]( https://github.com/android/security-samples/tree/master/BiometricLoginKotlin) ,前面说过生物认证只提供认证结果,那么结合 Login 业务,在官方的例子中 **`BiometricPrompt` 主要是用于做认证和加密的作用**: + +![image-20220329162115306](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image7) + +如上图所示,场景是在登陆之后,我们获取到了用户的 Token 信息,这个 Token 信息可能是服务器基于用户密码合并后的内容,所以它包含了一些敏感隐私,为了安全期间我们不能直接存储,而是利用 `BiometricPrompt` 去实现加密后存储: + +- 首先通过 `KeyStore`,主要是得到一个包含密码的 `SecretKey` ,当然这里有一个关键操作,那就是 `setUserAuthenticationRequired(true)`,后面我们再解释; +- 然后利用 `SecretKey` 创建 `Clipher` , `Clipher` 就是 Java 里常用于加解密的对象; +- 利用 `BiometricPrompt.CryptoObject(cipher)` 去调用生物认证授权; +- 授权成功后会得到一个 `AuthenticationResult` ,Result 里面包含存在密钥信息的 `cryptoObject?.cipher` 和 `cipher.iv` 加密偏移向量; +- 利用授权成功后的 `cryptoObject?.cipher` 对 Token 进行加密,然后和 `cipher.iv` 一起保存到 `SharePerferences` ,就完成了基于 `BiometricPrompt` 的加密保存; + +是不是觉得有点懵? 简单说就是:**我们通过一个只有用户通过身份验证时才授权使用的密钥来加密 Token ,这样不管这个 Token 是否泄漏,对于我们来说都是安全的。** + +然后在 `KeyStore` 逻辑里这里有个 `setUserAuthenticationRequired(true)` 操作,这个操作的意思就是:是否仅在用户通过身份验证时才授权使用此密钥,也就是当设置为 `true` 时: + +**用户必须通过使用其锁屏凭据的子集(例如密码/PIN/图案或生物识别)向此 Android 设备进行身份验证,才能够而授权使用密钥。** + +也就是只有设置了安全锁屏时才能生成密钥,而一旦安全锁屏被禁用(重新配置为无、不验证用户身份的模式、被强制重置)时,密钥将*不可逆转地失效。* + +> 另外可以设置了 `setUserAuthenticationValidityDurationSeconds` 来要求密钥必须至少有一个生物特征才可用,而一但它设置为 true,如果用户注册了新的生物特征,它也将不可逆转地失效。 + +**所以可以看到,这个流程下密钥会和系统安全绑定到一起,从而不害怕 Token 等信息的泄漏**,也因为授权成功后的 `CryptoObject` 和 `KeyStore` 集成到一起,可以更有效地抵御例如 root 的攻击。 + +而反之获取的流程也是类似,如下图所示: + +- 在 `SharePerferences` 里获取加密后的 Token 和 iv 信息; +- 同样是利用 `SecretKey` 创建 `Clipher` ,不过这次要带上保存的 iv 信息; +- 利用 `BiometricPrompt.CryptoObject(cipher)` 去调用生物认证授权; +- 通过授权成功后的 `cryptoObject?.cipher` 对 Token 进行加密,得到原始的 Token 信息; + + + +![image-20220329162133600](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image8) + + + +所以可以看到,**基本思路就是利用 `BiometricPrompt` 认证后得到 `CryptoObject?.Cipher` 去加解密,通过系统的安全等级要保护我们的隐私信息**。 + +最后补充一个知识点,虽然一般我们不关心,但是在 `BiometricPrompt` 里有 ***auth-per-use*** 和 ***time-bound*** 这两个概念: + +- ***auth-per-use*** 密钥要求每次使用密钥时,都必须进行认证 ,前面我们通过 `BiometricPrompt.CryptoObject(cipher)` 去调用授权方法就是这类实现; +- ***time-bound*** 密钥是一种在一定的时间段内有效的密钥,可以通过 `setUserAuthenticationValidityDurationSeconds` 设置有效时长,如果你设置为很短,例如 5 秒,那行为上和 auth-per-use 基本类似; + +> 更多资料可以参考官方的 [biometric-authentication-on-android](https://medium.com/androiddevelopers/biometric-authentication-on-android-part-1-264523bce85d) + +### 1.4、Tencent soter + +前面说到 Android 上还有 soter ,腾讯在微信指纹支付全流程之上,将它的流程抽象为一套完备的生物识别标准:SOTER。 + +SOTER 会与手机厂商合作,在系统原有的接口能力之上提供安全加固,通过业务无关的安全域(TEE,即独立于手机操作系统的安全区域,root或越狱无法访问到)应用程序(TA)降低开发难度和适配成本,**做到即使外部环境不可信,依然可以安全授权。** + +> TEE(Trusted Execution Environment)是独立于手机操作系统的一块独立运行的安全区域,SOTER标准中,所有的密钥生成、数据签名处理、指纹验证、敏感数据传输等敏感操作均在 TEE 中进行,**并且 SOTER使用的设备根密钥由厂商在产线上烧入,从根本上解决了根密钥不可信的问题,并以此根密钥为信任链根,派生密钥,从而完成**,与微信合作的所有手机厂商将均带有硬件TEE,并且通过腾讯安全平台和微信支付安全团队验收,符合SOTER标准。 + +![image-20220329214046518](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image9) + +简而言之,这是一个支持直通厂商,并且具备后台服务对接校验的第三方库,目前最近 5 个月都还有在更新,那它有什么问题呢? + +那就是**必须是与微信合作的所有手机厂商和机型才能正常使用** ,而且经常在一些厂商系统上出现奇奇怪怪的问题,比如: + +- MiUI13 绑定服务异常; +- 鸿蒙系统API层面报错; +- 莫名其妙地出现崩溃; + +但是它可以实现基本类似于微信支付的能力,所以如何取舍就看你的业务需求了。 + +> 支持机型可查阅 :[#有多少设备已经支持tencent-soter](https://github.com/Tencent/soter/wiki#有多少设备已经支持tencent-soter) + + + +## iOS + +相对来说 iOS 上的生物认证就舒适不少,相比较 Android 上需要区分系统版本和厂商的 `fingerprint` 、`face` 和 `iris` ,iOS 上的 Face ID 和 Touch ID 就十分统一和简洁。 + +简单介绍下 iOS 上使用生物认证,首先需要在 `Info.plist` 文件添加描述信息: + +```xml +NSFaceIDUsageDescription +Why is my app authenticating using face id? +``` + +然后导入头文件 `#import ` ,最后创建 `LAContext` 去执行授权操作,这里也简单展示对应的错误码: + +| Error Code | Type | +| --------------------------- | --------------------------------- | +| LAErrorSystemCancel | 系统取消了授权,比如有其他APP切换 | +| LAErrorUserCancel | 用户取消验证 | +| LAErrorAuthenticationFailed | 授权失败 | +| LAErrorPasscodeNotSet | 系统未设置密码 | +| LAErrorBiometryNotAvailable | ID不可用,例如未打开 | +| LAErrorBiometryNotEnrolled | ID不可用,用户未录入 | +| LAErrorUserFallback | 用户选择输入密码 | + +而同样关于自定义 UI 问题上,想必大家都知道了,**iOS 生物认证没有自定义 UI 的说法,也不支持自定义 UI ,系统怎么样就怎么样,你可以做的只有类似配置‘是否允许使用密码授权’这样的行为** 。 + +> 在这一点上相信 Android 开发都十分羡慕 iOS ,有问题也是系统问题,无法修复。 + +同样,简单说说在 iOS 上使用生物识别的 Login 场景流程: + +- 获取到 Token 信息后,验证用户的 TouchID/FaceID ; +- 验证通过后,将 Token 等信息保存到 keychain (keychain 只是一个数据存储,用于存储一些敏感数据如密码、证书等); +- 保存成功后,下次再次登录时通过验证 TouchID/FaceID 获取对应信息; + +![image-20220329223329042](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIO/image10) + +这里主要有两个关键点: + +- **访问级别** : 例如是否需要每次都进行身份验证时才可以访问项目; +- **身份验证级别**: 也就是什么场景下可以访问到存储的信息; + +举个例子,访问 keychain 首先是需要创建 accessControl ,一般可以通过 **`SecAccessControlCreateWithFlags`** 来创建 accessControl ,这里有个关键参数用于指定访问级别: + +- kSecAttrAccessibleAfterFirstUnlock 开机之后密钥不可用,需要等用户输入开机密码 +- kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: 开机之后密钥不可用,需要等用户输入开机密码,但是仅限于当前设备 +- kSecAttrAccessibleWhenUnlocked: 解锁过的设备密钥会保持可用状态 +- kSecAttrAccessibleWhenUnlockedThisDeviceOnly: 解锁过的设备密钥会保持可用状态,仅当前设备 +- kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly: 解锁过的设备密钥会保持可用状态,只有用户设置密码后密钥才可用 +- kSecAttrAccessibleAlways: 始终可用,已经 Deprecated +- kSecAttrAccessibleAlwaysThisDeviceOnly: 密钥始终可用,但无法迁移到其他设备,已经 Deprecated + +类似场景下一般使用 `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` ,另外还有 `SecAccessControlCreateFlags`标志,它主要是用于指定希望用户在访问钥匙串时的约束,一般类似场景会使用 `userPresence` : + +- devicePasscode: 限制使用密码访问 +- biometryAny: 使用任何已注册 touch 或 face ID 访问 +- biometryCurrentSet: 限制使用当前注册 touch 或 face ID 访问 +- userPresence: 限制使用生物特征或密码访问 +- watch: 使用手表访问 + +创建完成 accessControl 之后,通过设置 kSecAttrAccessControl 后正常把信息存储到 keychain 就可以了,在存储 keychain 时也有可选的 `kSecClass` ,一般选用 `kSecClassGenericPassword`: + +- kSecClassGenericPassword: 通用密码 +- kSecClassInternetPassword: Internet 密码 +- kSecClassCertificate:证书 +- kSecClassKey:加密密钥 +- kSecClassIdentity: 身份认证 + +*当然,此时你是否发现,在谈及 accessControl 和 keychain 时没有说明 `LAContext` ?* + +其实在创建 accessControl 时是有对应 `kSecUseAuthenticationContext` 参数用于设置 `LAContext ` 到 keychain 认证,但是也可以不设置,具体为: + +- 如果未指定,并且该项目需要 authentication 认证,那就会自动创建一个新的 `LAContext ` ,使用一次后丢弃; +- 如果是使用先前已通过身份验证的 `LAContext ` ,则操作直接成功而不要求用户进行身份验证; +- 如果是使用先前未经过身份验证的 `LAContext ` ,则系统会尝试在该 `LAContext ` 上进行身份验证,如果成功就可以在后续的钥匙串操作中重用。 + +> 更多可见官方的: [accessing_keychain_items_with_face_id_or_touch_id](https://developer.apple.com/documentation/localauthentication/accessing_keychain_items_with_face_id_or_touch_id/) + +可以看到, iOS 上都只需要简单地配置就行了,因为系统层面也不会给你多余的能力。 + +> ⚠️提示,如果你有需要屏蔽 iOS 在生物验证失败之后,不展示输入密码的选项,可以配置 `LAContext ` 的 `context.localizedFallbackTitle=""` 来实现。 + +## 三、最后 + +虽然本篇从头到位并没有教你如何使用 Android 或者 iOS 的生物认证,但是作为汇总资料,本篇基本覆盖了 Android 或者 iOS 生物认证相关的基本概念和问题,相信本篇将会特别适合正在调研生物认证相关开发的小伙伴。 + +最后,还是惯例,如果对于这方便你有什么问题或者建议,欢迎留言评论交流。 \ No newline at end of file diff --git a/Flutter-BIOS.md b/Flutter-BIOS.md new file mode 100644 index 0000000..f252d2b --- /dev/null +++ b/Flutter-BIOS.md @@ -0,0 +1,94 @@ +# Flutter iOS OC 混编 Swift 遭遇动态库和静态库问题填坑 + + +Flutter 在 iOS 上的编译问题相信大家多多少少遇到过,不知道大家在搜索这方便的问题时,得到的答案是不是*让你 clean 或者 install 多几次*,很多时候就算解决完问题,也是处于薛定谔的状态,所以**本篇也简单记录下 Flutter 开发中,OC 混编 Swift 遭遇动态库和静态库的问题**,希望对“蒙圈”中的你有点帮助。 + +![image-20220422091858815](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image1) + + + +首先,当我在一个 OC 项目里接入一个 Swift 插件,可能会遇到什么问题? + +如下图所示,**如果你是一个比较老的 Flutter 项目,那可能会出现 swift 插件出现 not found 的问题**。 + +![image-20220422093205569](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image2) + +针对这个问题,一般都是建议在 Podfile 文件下添加 `use_frameworks!` ,有时候还会建议添加 `use_modular_headers!` ,那这两个标记位的作用是什么? + +```shell +target 'Runner' do + use_frameworks! + use_modular_headers! + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end +``` + +> 我们知道 Podfile 的作用是处理 CocoaPads ,而 `use_frameworks!`告诉 CocoaPods 你想使用 Framework 而不是静态库,而默认由于 Swift 不支持静态库,因此有一开始 Swift 必须使用 Framework 的限制。 + +静态库和 Framework 的区别在于: + +- *.a 的静态库类似于编译好的机械代码,源代码和库代码都被整合到单个可执行文件中,所以它会和设备架构绑定,并且不包含资源文件比如图片; +- Framework 支持将动态库、头文件和资源文件封装到一起的一种格式,其中动态库的简单理解是:不会像静态库一样被整合到一起,而是在运行或者运行时动态链接; + +另外一个配置 `use_modular_headers!` ,它主要是将 pods 转为 Modular,因为 Modular 是可以直接在 Swift中 import ,所以不需要再经过 bridging-header 的桥接。 + +> 但是开启 `use_modular_headers!` 之后,会使用更严格的 header 搜索路径,开启后 pod 会启用更严格的搜索路径和生成模块映射,历史项目可能会出现重复引用等问题,因为在一些老项目里 CocoaPods 是利用Header Search Paths 来完成引入编译,当然使用 `use_modular_headers!`可以提高加载性能和减少体积。 + +继续回到问题上,我们在添加完 `use_frameworks!` 之后,有一定几率中奖各种 *Undefined symbol* 的错误问题,这时候不要慌,因为这是 Swfit 里有静态库导致。 + +![image-20220422103410759](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image3) + +很明显 Swift 不支持静态库的行为不科学,所以从 Xcode 9 开始 Swift 就开始支持静态库,而 CocoaPods 1.9.0 开始,引入了 **`use_frameworks! :linkage => :static`** 来生支持有静态库和 Framework 的情况。 + +所以修改 use_frameworks 配置,增加 static 之后可以看到 *Undefined symbol* 的错误都消失了,但是运行之后,可能会喜提新的问题: *non-modular header* 。 + +![image-20220422104501881](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image4) + +如果你去搜索答案,有很多答案会告诉你如下图所示,通过 `Allow Non-modular Includes in Framework Modules` 设置为 `true` 就可以解决问题,**但是很明显这并不是正解,它更多适用于临时的紧急状体下**。 + +![image-20220422105705987](http://img.cdn.guoshuyu.cn/20220627_Flutter-BIOS/image5) + +当然,你也可以在出现问题的插件的 `.podspec` 下单独配置 ALLOW ,效果相同,更轻量级,但是这也只是临时解决方案。 + +```shell +s.user_target_xcconfig = { 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES' } +``` + +为什么说这种方式不靠谱,因为你不知道官方会什么时候删除这种允许,当然这个问题网友提供的解决方案其实千奇百怪: + +- 如果是 App 使用 **dynamic framework** 里的 **header** 导致错误,可以使用 `#import "MyFile.h"` 而不是 `#import ` ; +- 将`#import`语句移到 `.m`(而不是将其放在`.h`头文件中), 这样它就不会有包含 *non-modular header* 的问题,例如: https://github.com/AFNetworking/AFNetworking/pull/2206/files ; +- 重命名 header ,不要让 header 和模块名一样,变为 `#import ` +- 在 build setting 配置 OTHER_SWIFT_FLAGS -Xcc -Wno-error=non-modular-include-in-framework-module 解决 Swift 的问题; + +**有可能它们都能解决你的问题,但是为什么呢?下次遇到这些问题要选哪个解决?** + +回归到我们的问题,其实我的问题关键是:**不能在 Framework Module 中使用非 Modular 的 Header**,也就问题是在 Framework Module 中加载了非当前 Module 的头文件,而由于 Header 是对外 public ,比如配置到了 `s.public_header_files` ,就会导致非 Modular 的 Header 也出现对外暴露的风险,所以我这边的解放方式也很简单: + +**在 `s.public_header_files` 里只放需要公开的 *Plugin.h ,使用了非 Modular 的 Header 不对外 public,从而符合规范达到编译成功**。 + +所以这里面的核心是:不要在 Umbrella Header File 中引用不需要对外公开的 OC 头文件去作为子 module ,这也解释了为什么上面讲出问题的 `#import`语句移到 `.m` 就解决问题的逻辑。 + +> 例如有时候你还会引用一些系统的 C Module ,其实在 Framework Module 化过程中也会有类似的问题。 + +所以知道了为什么和怎么解决,就不会只是粗暴通过 LLVM 的配置来设置 `Allow Non-modular Includes in Framework Modules` 去解决薛定谔的问题。 + +另外你可能还有用到的,比如模拟器编译提示 unsupport arm64、 BITCODE 失败,SWIFT_VERSION 版本冲突等等: + +```shell +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + # building for iOS Simulator, but linking in an object file built for iOS, for architecture ‘arm64’ + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' + #不支持 BITCODE + config.build_settings['ENABLE_BITCODE'] = 'NO' + #解决swift模块问题 + config.build_settings['SWIFT_VERSION'] = '5.0' + end + end +end +``` + +当然,最后一句话:**珍爱头发,远离 Swift 混编**。 \ No newline at end of file diff --git a/Flutter-DWN.md b/Flutter-DWN.md new file mode 100644 index 0000000..4196e8b --- /dev/null +++ b/Flutter-DWN.md @@ -0,0 +1,205 @@ +# 掘金x得物公开课 - Flutter 3.0下的混合开发演进 + +hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 项目的负责人郭树煜,同时也是今年新晋的 [Flutter GDE](https://juejin.cn/post/7102242694755254279),借着本次 Google I/O 之后发布的 Flutter 3.0,来和大家聊一聊 Flutter 里混合开发的技术演进。 + +为什么混合开发在 Flutter 里是特殊的存在?因为它渲染的控件是通过 Skia 直接和 GPU 交互,也就是说 Flutter 控件和平台无关,甚至连 UI 绘制线程都和原生平台 UI 线程是相互独立,**所以甚至于 Flutter 在诞生之初都不支持和原生平台的控件进行混合开发,也就是不支持 `WebView` ,这就成了当时最大的缺陷之一** 。 + +其实从渲染的角度看 Flutter 更像是一个 2D 游戏引擎,事实上 Flutter 在这次 Google I/O 也分享了基于 [Flutter 的游戏开发 ToolKit 和第三方工具包 Flame](https://juejin.cn/post/7103284735010406407) ,如图所示就是本次 Google I/O 发布的 Pinball 小游戏,所以从这些角度上看都可以看出 Flutter 在混合开发的特殊性。 + +> **如果说的更形象简单一点,那就是如何把原生控件渲染到 `WebView` 里**。 + + + +![TT](http://img.cdn.guoshuyu.cn/20220626_DWN/image2.gif) + +# 最初的社区支持 + +不支持 `WebView` 在最初可以说是 Flutter 最大的痛点之一,所以在这样窘迫的情况下,社区里涌现出一些临时的解决方法,比如 `flutter_webview_plugin` 。 + +类似 `flutter_webview_plugin` 的出现,解决了当时大部分时候 App 里打开一个网页的简单需求,如下图所示,它的思路就是: + +> 在 Flutter 层面放一个占位控件提供大小,然后原生层在同样的位置把 ` WebView` 添加进去,从而达到看起来把 ` WebView` 集成进去的效果,**这个思路在后续也一直被沿用**。 + +![image-20220625170833702](http://img.cdn.guoshuyu.cn/20220626_DWN/image3.png) + +**这样的实现方式无疑成本最低速度最快,但是也带来了很多的局限性**。 + +相信大家也能想到,**因为 Flutter 的所有控件都是渲染一个 `FlutterView` 上,也就是从原生的角度其实是一个单页面的效果**,所以这种脱离 Flutter 渲染树的添加控件的方法,无疑是没办法和 Flutter 融合到一起,举个例子: + +- 如图一所示,从 Flutter 页面跳到 Native 页面的时候,打开动画无法同步,因为 `AppBar` 是 Flutter 的,而 Native 是原生层,它们不在同一个渲染树内,所以无法实现同步的动画效果 +- 如图二所示,比如在打开 Native 页面之后,通过 `Appbar` 再打开一个黄色的 Bottm Sheet ,可以看到此时黄色的 Bottm Sheet 打开了,但是却被 Native 遮挡住(Demo 里给 Native 设置了透明色),因为 Flutter 的 Bottm Sheet 是被渲染在 `FlutterView` 里面,而 Native UI 把 `FlutterView` 挡住了,所以新的 `Flutter UI` 自然也被遮挡 +- 如图三所示,当我们通过 reload 重刷 Flutter UI 之后,可以看到 Flutter 得 UI 都被重置了,但是此时 Native UI 还在,因为此时已经没有返回按键之类的无法关闭,这也是这种集成方式一不小心就影响开发的问题 +- 如图四通过 iOS 上的 debug 图层,我们可以更形象地看到这种方式的实现逻辑和堆叠效果 + +| 动画不同步 | 页面被挡 | reload 之后 | iOS | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![11111111](http://img.cdn.guoshuyu.cn/20220626_DWN/image4.gif) | ![222222222](http://img.cdn.guoshuyu.cn/20220626_DWN/image5.gif) | ![333333](http://img.cdn.guoshuyu.cn/20220626_DWN/image6.gif) | ![image-20220616142126589](http://img.cdn.guoshuyu.cn/20220626_DWN/image7.png) | + +# PlatformView + +随着 Flutter 的发展,官方支持混合开发势在必行,所以第一代 `PlatformView` 的支持还是诞生了,但是由于 Android 和 iOS 平台特性的不同,最初Android 的 `AndroidView` 和 iOS 的 `UIKitView` 实现逻辑相差甚远,**以至于后面 Flutter 的 `PlatformView` 的每次大调整都是围绕于 Android 在做优化** 。 + +### Android + +最初 Flutter 在 Android 上对 `PlatformView` 的支持是通过 `VirtualDisplay` 实现,`VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,`VirtualDisplay` 一般在副屏显示或者录屏场景下会用到,而在 Flutter 里 `VirtualDisplay` 会将虚拟显示区域的内容渲染在一个内存 `Surface`上。 + +**在 Flutter 中通过将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 中 ,然后通过 textureId 在 `VirtualDisplay` 对应的内存中提取绘制的纹理**, 简单看实现逻辑如下图所示: + +![image-20220626151538054](http://img.cdn.guoshuyu.cn/20220626_DWN/image8.png) + +> 这里其实也是类似于最初社区支持的模式:通过在 Dart 层提供一个 `AndroidView` ,从而获取到控件所需的大小,位置等参数,当然这里多了一个 `textureId` ,这个 id 主要是提交给 Flutter Engine ,通过 id Flutter 就可以在渲染时将画面从内存里提出出来。 + +### iOS + +在 iOS 平台上就不使用类似 `VirtualDisplay` 的方法,而是**通过将 Flutter UI 分为两个透明纹理来完成组合**,这种方式无疑更符合 Flutter 社区的理念,这样的好处是: + +> 需要在 `PlatformView` 下方呈现的 Flutter UI 可以被绘制到其下方的纹理;而需要在 `PlatformView` 上方呈现的 Flutter UI 可以被绘制到其上方的纹理, 它们只需要在最后组合起来就可以了。 + +是不是有点抽象? + +简单看下面这张图,其实就是通过在 `NativeView` 的不同层级设置不同的透明图层,然后把不同位置的控件渲染到不同图层,最终达到组合起来的效果。 + +![image-20220626151526444](http://img.cdn.guoshuyu.cn/20220626_DWN/image9.png) + +那明明这种方法更好,为什么 Android 不一开始也这样实现呢? + +因为当时在实现思路上, `VirtualDisplay` 的实现模式并不支持这种模式,因为在 iOS 上框架渲染后系统会有回调通知,例如:*当 iOS 视图向下移动 `2px` 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 `2px`*。 + +但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染。**如果强行以这种方式在 Android 上使用,最终将产生很多如 `AndroidView` 与 Flutter UI 不同步的问题**。 + +### 问题 + +事实上 `VirtualDisplay` 的实现方式也带来和很多问题,简单说两个大家最直观的体会: + +#### 触摸事件 + +因为控件是被渲染在内存里,虽然**你在 UI 上看到它就在那里,但是事实上它并不在那里**,你点击到的是 `FlutterView `,所以**用户产生的触摸事件是直接发送到 `FlutterView`**。 + +所以触摸事件需要在 `FlutterView` 到 Dart ,再从 Dart 转发到原生,然后如果原生不处理又要转发回 Flutter ,如果中间还存在其他派生视图,事件就很容易出现丢失和无法响应,而这个过程对于 `FlutterView` 来说,在原生层它只有一个 View 。 + +所以 Android 的 `MotionEvent` 在转化到 Flutter 过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。 + +#### 文字输入 + +一般情况下 **`AndroidView` 是无法获取到文本输入,因为 `VirtualDisplay` 所在的内存位置会始终被认为是 `unfocused` 的状态**。 + +> `InputConnections` 在 `unfocused` 的 View 中通常是会被丢弃。 + +所以 **Flutter 重写了 `checkInputConnectionProxy` 方法,这样 Android 会认为 `FlutterView` 是作为 `AndroidView` 和输入法编辑器(IME)的代理**,这样 Android 就可以从 `FlutterView` 中获取到 `InputConnections` 然后作用于 `AndroidView` 上面。 + +> 在 Android Q 开始又因为非全局的 `InputMethodManager` 需要新的兼容 + +当然还有诸如性能等其他问题,但是至少先有了支持,有了开始才会有后续的进阶,在 Flutter 3.0 之前, `VirtualDisplay` 一直默默在 `PlatformView` 的背后耕耘。 + +# HybridComposition + +时间来到 Flutter 1.2,Hybrid Composition 是在 Flutter 1.2 时发布的 Android 混合开发实现,它使用了类似 iOS 的实现思路,提供了 Flutter 在 Android 上的另外一种 `PlatformView` 的实现。 + +如下图是在 Dart 层使用 `VirtualDisplay` 切换到 `HybridComposition` 模式的区别,最直观的感受应该是需要写的 Dart 代码变多了。 + +![111111](http://img.cdn.guoshuyu.cn/20220626_DWN/image10.png) + +但是其实 `HybridComposition` 的实现逻辑是变简单了: **`PlatformView` 是通过 `FlutterMutatorView` 把原生控件 `addView` 到 `FlutterView` 上,然后再通过 `FlutterImageView` 的能力去实现图层的混合**。 + +> 又懵了?不怕,马上你就懂了 + +简单来说就是 `HybridComposition` 模式会直接把原生控件通过 `addView` 添加到 `FlutterView` 上 。**这时候大家可能会说,咦~这不是和最初的实现一样吗?怎么逻辑又回去了** ? + +> 其实确实是社区的进阶版实现,Flutter 直接通过原生的 `addView` 方法将 `PlatformView` 添加到 `FlutterView` 里,而当你还需要在 `PlatformView` 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 `FlutterImageView` 来承载这个 Widget 的纹理。 + +举一个简单的例子,如下图所示,一个原生的 `TextView` 被通过 `HybridComposition` 模式接入到 Flutter 里(`NativeView`),而在 Android 的显示布局边界和 Layout Inspector 上可以清晰看到: **灰色 `TextView` 通过 `FlutterMutatorView` 被添加到 `FlutterView` 上被直接显示出来** 。 + +![image-20220618152055492](http://img.cdn.guoshuyu.cn/20220626_DWN/image11.png) + +**所以在 `HybridComposition` 里 `TextView` 是直接在原生代码上被 add 到 `FlutterView` 上,而不是提取纹理**。 + +那如果我们看一个复杂一点的案例,如下图所示,其中蓝色的文本是原生的 `TextView` ,红色的文本是 Flutter 的 `Text` 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到: + +- 两个蓝色的 `TextView` 是通过 `FlutterMutatorView` 被添加在 `FlutterView` 之上,并且把没有背景色的红色 RE 遮挡住了 +- 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 `TextView` 之上,所以这时候多一个 `FlutterImageView` ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。 + +![image-20220616165047353](http://img.cdn.guoshuyu.cn/20220626_DWN/image12.png) + +**可以看到 `Hybrid Composition` 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生角度看它就是原生层面的物理堆叠,需要都一个层级就多加一个 `FlutterImageView` ,同一个层级的 Flutter 控件共享一个 `FlutterImageView`** 。 + +当然,在 `HybridComposition` 里 `FlutterImageView` 也是一个很有故事的对象,由于篇幅原因这里就不详细展开,这里大家可以简单看这张图感受下,也就是在有 `PlatformView` 和没有 `PlatformView` 是,Flutter 的渲染会有一个转化的过程,而**在这个变化过程,在 Flutter 3.0 之前可以通过 ` PlatformViewsService.synchronizeToNativeViewHierarchy(false);` 取消**。 + +![image-20220618153757996](http://img.cdn.guoshuyu.cn/20220626_DWN/image13.png) + +最后,Hybrid Composition 也不少问题,比如上面的转化就是为了解决动画同步问题,当然这个行为也会产生一些性能开销,例如: + +> 在 Android 10 之前, *Hybrid Composition* 需要将内存中的每个 Flutter 绘制的帧数据复制到主内存,之后再从 GPU 渲染复制回来 ,所以也会导致 *Hybrid Composition* 在 Android 10 之前的性能表现更差,例如在滚动列表里每个 Item 嵌套一个 *Hybrid Composition* 的 `PlatformView` ,就可能会变卡顿甚至闪烁。 + +其他还有线程同步,闪烁等问题,由于篇幅就不详细展开,如果感兴趣的可以详细看我之前发布过的 [《Flutter 深入探索混合开发的技术演进》](https://juejin.cn/post/7093858055439253534) 。 + + + +# TextureLayer + +随着 Flutter 3.0 的发布,第一代 `PlatformView` 的实现 `VirtualDisplay` 被新的 `TextureLayer` 所替代,如下图所示,简单对比 `VirtualDisplay` 和 `TextureLayer` 的实现差异,**可以看到主要还是在于原生控件纹理的提取方式上**。 + + + +![image-20220618154327890](http://img.cdn.guoshuyu.cn/20220626_DWN/image14.png) + +从上图我们可以得知: + +- 从 `VirtualDisplay` 到 `TextureLayer` , **Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑**; +- 以前 Flutter 中会将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` ,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到;**现在 `AndroidView` 需要的内容,会通过 View 的 `draw` 方法被绘制到 `SurfaceTexture` 里,然后同样通过 `TextureId` 获取绘制在内存的纹理** ; + +是不是又有点蒙?简单说就是不需要绘制到副屏里,现在直接通过 override `View` 的 `draw` 方法就可以了。 + +在 *TextureLayer* 的实现里,同样是需要**把控件添加到一个 `PlatformViewWrapper` 的原生布局控件里,但是这个控件通过 override 了 `View` 的 `draw` 方法,把原本的 Canvas 替换成 `SurfaceTexture` 在内存的 Canvas ,所以 `PlatformViewWrapper` 的 child 会把控件绘制到内存的 `SurfaceTexture` 上。** + +![](http://img.cdn.guoshuyu.cn/20220626_DWN/image15.png) + +举个例子,还是之前的代码,如下图所示,这时候通过 *TextureLayer* 模式运行之后,通过 Layout Inspector 的 3D 图层可以看到,两个原生的 `TextView` 通过 `PlatformViewWrapper` 被添加到 ` FlutterView` 上。 + +但是不同的是,**在 3D 图层里看不到 `TextView` 的内容,因为绘制 `TextView` 的 Canvas 被替换了**,所以 `TextView` 的内容被绘制到内存的 Surface 上,最终会在渲染时同步 Flutter Engine 里。 + +![](http://img.cdn.guoshuyu.cn/20220626_DWN/image16.png) + +看到这里,你可能也发现了,这时候因为有 `PlatformViewWrapper` 的存在,点击会被 `PlatformViewWrapper` 内部拦截,从而也解决了触摸的问题, 而这里刚好有人提了一个问题,如下图所示: + +> "从图 1 Layout Inspector 看, `PlatformWrapperView` 是在 `FlutterSurfaceView` 上方,为什么如图 2 所示,点击 Flutter button 却可以不触发 native button的点击效果?"。 + +| 图1 | 图2 | +| ------------------------------------------------------------ | ------------------------------------------------------- | +| ![image.png](http://img.cdn.guoshuyu.cn/20220626_DWN/image17) | ![img](http://img.cdn.guoshuyu.cn/20220626_DWN/image18) | + +思考一下,因为最直观的感受:**点击不都是被 `PlatformViewWrapper` 拦截了吗?明明 `PlatformViewWrapper` 是在 `FlutterSurfaceView` 之上,为什么 `FlutterSurfaceView` 里的 FlutterButton 还能被点击到**? + +这里简单解释一下: + +- 1、首先那个 Button 并不是真的被摆放在那里,而是通过 `PlatformViewWrapper` 的 `super.draw`绘制到 surface 上的,所以在那里的是 `PlatformViewWrapper` ,而不是 Button ,**Button 的内容已经变成纹理去到了 `FlutterSurfaceView` 里面**。 +- 2、 **`PlatformViewWrapper` 里重写了 `onInterceptTouchEvent` 做了拦截**,`onInterceptTouchEvent` 这个事件是从父控件开始往子控件传,因为拦截了所以不会让 Button 直接响应,然后在 `PlatformViewWrapper` 的 `onTouchEvent` 响应里是做了点击区域的分发,响应会分发到了 `AndroidTouchProcessor` 之后,会打包发到 `_unpackPointerDataPacket` 进入 Dart +- 3、 在 Dart 层的点击区域,如果没有 Flutter 控件响应,会是 `_PlatformViewGestureRecognizer`-> `updateGestureRecognizers` -> `dispatchPointerEvent` -> `sendMotionEvent` 又发送回原生层 +- 4、回到原生 `PlatformViewsController` 的 `createForTextureLayer` 里的 `onTouch` ,执行 `view.dispatchTouchEvent(event);` + +![image-20220625171101069](http://img.cdn.guoshuyu.cn/20220626_DWN/image19.png) + +总结起来就是:**`PlatfromViewWrapper` 拦截了 Event ,通过 Dart 做二次分发响应,从而实现不同的事件响应 ** ,它和 VirtualDisplay 的不同是, VirtualDisplay 的事件响应都是在 `FlutterView` 上,但是TextureLayout 模式,是有独立的原生 `PlatfromViewWrapper` 控件来开始,所以区域效果和一致性会更好。 + +### 问题 + +最后这里还需要提个醒,如果你之前使用的插件使用的是 `HybirdComposition ` ,但是没做兼容,也就是使用的还是 `PlatformViewsService.initSurfaceAndroidView` 的话,它也会切换成 `TextureLayer` 的逻辑,**所以你需要切换为 `PlatformViewsService.initExpensiveAndroidView` ,才能继续使用原本 `HybirdComposition ` 的效果**。 + +> ⚠️我也比较奇怪为什么 Flutter 3.0 没有提及 Android 这个 breaking change ,因为对于开发来说其实是无感的,不小心就掉坑里。 + +那你说为什么还要 `HybirdComposition ` ? + +前面我们说过, `TextureLayer` 是通过在 `super.draw` 替换 Canvas 的方法去实现绘制,但是它替换不了 `Surface` 里的一些 Canvas ,所以比如一些需要 `SurfaceView` 、`TextureView` 或者有自己内部特殊 `Canvas` 的场景,你还是需要 `HybirdComposition ` ,只不过可能会和官方新的 API 名字一样,它 Expensive 。 + +Expensive 是因为在 Flutter 3.0 正式版开始,`FlutterView` 在使用 `HybirdComposition ` 时一定会 converted to `FlutterImageView` ,这也是 Flutter 3.0 下一个需要注意的点。 + +![image-20220616170253242](http://img.cdn.guoshuyu.cn/20220626_DWN/image20.png) + +> 更多内容可见 [《Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer》](https://juejin.cn/post/7098275267818291236) + +![image-20220625164049356](http://img.cdn.guoshuyu.cn/20220626_DWN/image21.png) + +# 最后 + +最后做个总结,可以看到 Flutter 为了混合开发做了很多的努力,特别是在 Android 上,也是因为历史埋坑的原因,由于时间关系这里没办法都详细介绍,但是相信本次之后大家对 Flutter 的 `PlatformView` 实现都有了全面的了解,这对大家在未来使用 Flutter 也会有很好的帮助,如果你还有什么问题,欢迎交流。 + +![image-20220626151444011](http://img.cdn.guoshuyu.cn/20220626_DWN/image22.png) \ No newline at end of file diff --git a/Flutter-DWW.md b/Flutter-DWW.md new file mode 100644 index 0000000..a2e4533 --- /dev/null +++ b/Flutter-DWW.md @@ -0,0 +1,661 @@ +# Flutter 深入探索混合开发的技术演进 + +关于 Flutter 混合 `PlatformView` 的实现已经介绍过两次,随着 5 月份谷歌 IO 的接近,新的 `PlatformView` 实现应该也会随之而来,本次就从头到尾来一个详细的关于 `PlatformView` 的演进总结。 + +> Flutter 作为新一代的跨平台框架,通过自定义渲染引擎的创新大大提高了跨平台的性能和一致性,但也正是因为这点, 相比之下 Flutter 在混合开发时对于原生控件的支持成本更高。 + +## Flutter 混合开发的难点 + +首先 Flutter 在混合开发中最大的难点就在于它独立的渲染引擎,举一个不是很恰当的例子: + +> Flutter 里混合开发类似与把原生控件渲染到 `WebView ` 里。 + +大致上在 Flutter 里混合开发的感觉就是这样,因为 **Flutter UI 不会转换为原生控件,而是由 Flutter Engine 使用 Skia 直接渲染在 `Surface` 上**。 + +所以 Flutter 在最早出来时并不支持 `WebView` 或 `MapView` 这些常用的控件,这也导致了当时 Flutter 一度的风评不大好,所以衍生出了第一代非官方的混合开发支持,例如: `flutter_webview_plugin `。 + +**在官方 `WebView` 控件支持出来之前** ,第三方是直接在 `FlutterView` 上覆盖了一个新的原生控件,利用 Dart 中的占位控件来**传递位置和大小**。 + +> Flutter 里几乎所有渲染都是渲染到 `FlutterView` 这样一个单页面上,所以直接覆盖一个新的原生 `WebView` 只能说缓解燃眉之急。 + +如下图,在 Flutter 端 `push` 出来一个 **设定好位置和大小** 的 `SingleChildRenderObjectWidget` ,从而得到需要显示的大小和位置,将这些信息通过 `MethodChannel` 传递到原生层,在原生层 `addContentView` 一个指定大小和位置的 `WebView` 。 + +![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image1.png) + +这样看起来就像是在 Flutter 中添加了 `WebView` ,但实际这样的操作只能说是“救急”,**因为这样的行为脱离了 Flutter 的渲染树**,其中一个问题就是: + +> 当你跳转 Flutter 其他页面的时候会被当前原生的 `WebView` 挡住;并且打开页面的动画时`Appbar` 和 `WebView` 难以保持一致,因为 `Appbar` 和 `WebView` 是出于两个动画体系和渲染体系。 + +就比如打开了新的 Flutter UI 2 页面,但是由于它还是在 `FlutterView` 内,所以它会被 `WebView` 所遮挡。 + +![image-20220307175357032](http://img.cdn.guoshuyu.cn/20220309_DWZB/image2.png) + +但是这个“占位”显示的思路,也起到了一定的作用,在后续 Flutter 支持原生 `PlatformView` 上起到了带头的作用。 + +## Flutter 初步支持原生控件 + +为了让 Flutter 真正走向大众化,官方开始推出了官方基于 `PlatformView` 的系列实现,比如: `webview_flutter` ,而这个实现 “缝缝补补” 也被沿用至今,成了 Flutter 接入原生的方式 之一。 + +### Android + +在 `PlatformView` 的整个实现中 Android 坑一直是最多的,因为一开始 Android 上主要是通过 `AndroidView` 做完成这项工作,而它的 *Virtual Displays* 实现其实并不友好。 + +**在 Flutter 中会将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` 中 ,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到**。 + +> `VirtualDisplay` 类似于一个虚拟显示区域,需要结合 `DisplayManager` 一起调用,一般在副屏显示或者录屏场景下会用到,在 `VirtualDisplay` 里会将虚拟显示区域的内容渲染在一个 `Surface` 上。 + +![image-20220307180859494](http://img.cdn.guoshuyu.cn/20220309_DWZB/image3.png) + +如上图所示,**简单来说就是原生控件的内容被绘制到内存里,然后 Flutter Engine 通过相对应的 `textureId` 就可以获取到控件的渲染数据并显示出来**,这个过程 `AndroidView` 这个占位控件提供了 size、offset 等位置和大小参数。 + +通过从 `VirtualDisplay` 获取纹理,并将其和 Flutter 原有的 UI 渲染树混合,使得 Flutter 可以在自己的 Flutter Widget tree 中以图形方式插入 Android 原生控件。 + +### iOS + +在 iOS 平台上就不使用类似 `VirtualDisplay` 的方法,而是**通过将 Flutter UI 分为两个透明纹理来完成组合:一个在 iOS 平台视图之下,一个在其上面**。 + +所以这样的好处就是: + +- 需要在 “iOS平台” 视图下方呈现的Flutter UI,最终会被绘制到其下方的纹理上; +- 而需要在 “平台” 上方呈现的 Flutter UI,最终会被绘制在其上方的纹理; + +iOS 上**它们只需要在最后组合起来就可以了**,通常这种方法更好,因为这意味着 Native View 可以直接添加到 Flutter 的 UI 层次结构中,但是可惜一开始 Android 平台并不支持这种模式。 + +### 问题 + +尽管前面可以使用 `VirtualDisplay` 将 Android 控件嵌入到 Flutter UI 中 ,但这种 `VirtualDisplay` 这种介入还有其他麻烦的问题需要处理。 + +#### 触摸事件 + +**默认情况下, `PlatformViews` 是没办法接收触摸事件**,因为 `AndroidView` 其实是被渲染在 `VirtualDisplay` 中 ,而每当用户点击看到的 `"AndroidView"` 时,其实他们就真正”点击的是正在渲染的 `Flutter` 纹理 ,**用户产生的触摸事件是直接发送到 Flutter View 中,而不是他们实际点击的 `AndroidView`**。 + +所以 `AndroidView` 使用 Flutter Framework 中检测用户的触摸是否在需要的特殊处理的区域内: + +> 当触摸成功时会向 Android embedding 发送一条消息,其中包含 touch 事件的详细信息。 + +这就变成有些本末倒置,触摸事件从原生-Flutter-原生,中间的转化导致某些信息被丢失,也导致了响应的延迟。 + +#### 文字输入 + +**`AndroidView` 是无法获取到文本输入,因为 `VirtualDisplay` 所在的位置会始终被认为是 `unfocused` 的状态**。 + +所以需要做一套代理来处理 `InputConnections` 做输入,甚至这个行为在 `WebView` 上更复杂,因为 `WebView` 具有自己内部的逻辑来创建和设置输入连接,而这些输入连接并没有完全遵循 Android 的协议。 + +### 同步问题 + +另外还需要处理各种同步问题,比如官方就创建了一个 `SurfaceTextureWrapper` 用于处理同步的问题。 + +因为当承载 `AndroidView` 的 `SurfaceTexture` 被释放时,由于 `SurfaceTexture.release` 是在 platform 线程被调用,而 `attachToGLContext `是在 raster 线程被调用,不同线程调用时可能导致:**当 `attachToGLContext `被调用时 texture 已经被释放了,所以需要 `SurfaceTextureWrapper` 用于实现 Java 里同步锁的效果**。 + + + +## Flutter Hybrid Composition + + + +所以经历了 *Virtual Display* 的折磨之后,官方终于在后续推出了更为合理的实现。 + +### 实现逻辑 + + *hybrid composition* 的出现给 Flutter 提供了一种新的混合思路,**那就是直接把原生控件添加到 Flutter 里一起组合渲染**。 + +首先简单介绍下使用,比起 *Virtual Display* 直接使用 `AndroidView` ,*hybrid composition* 相对会复杂一点点,dart 里使用到 `PlatformViewLink` 、`AndroidViewSurface` 、 `PlatformViewsService` 这三个对象。 + +正常在 dart 层面,使用 *hybrid composition* 接入原生控件: + +- 通过 `PlatformViewLink` 的 `viewType` 注册了一个和原生层对应的注册名称,这和之前的 `PlatformView` 注册一样; +- 然后在 `surfaceFactory` 返回一个 `AndroidViewSurface` 用于处理绘制和接收触摸事件,同时也是一个类似占位的作用; +- 最后在 `onCreatePlatformView` 方法使用 `PlatformViewsService` 初始化 `AndroidViewSurface` 和初始化所需要的参数,同时通过 Engine 去触发原生层的显示。 + +```dart +Widget build(BuildContext context) { + // This is used in the platform side to register the view. + final String viewType = 'hybrid-view-type'; + // Pass parameters to the platform side. + final Map creationParams = {}; + + return PlatformViewLink( + viewType: viewType, + surfaceFactory: + (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); +} +``` + +看起来好像是把一个 `AndroidView` 完成的事情变得相对复杂了,但是其实 *hybrid composition* 的实现相比其实更好理解。 + +使用 *hybrid composition* 之后, **`PlatformView` 就直接通过 `FlutterMutatorView`(一个特殊的 `FrameLayout`) 把原生控件 `addView` 到 `FlutterView`上,然后再通过 `FlutterImageView` 的能力去实现多图层的混合**。 + +不理解吗?没事,我们后面会详细介绍,先简单解释就是: + +> Flutter 只直接通过原生的 `addView` 方法将 `PlatformView` 添加到 `FlutterView` ,这就不需要什么 `surface `渲染再去获取的开销,而当你还需要再 `PlatformView` 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 `FlutterImageView` 来承载这个 Flutter Widget 。 + +![image-20220308173844917](http://img.cdn.guoshuyu.cn/20220309_DWZB/image4.png) + + + +### 深入例子详解 + + + +接下来让我们从实际例子去理解 *Hybrid Composition* ,结合 Andriod Studio 的 Layout Inspector,并开启手机的绘制边界来看会更直观。 + +如下代码所示,一般情况下我们运行之后会看到一片黑色,因为这时候 `FlutterView` 只有一个 `FlutterSurfaceView` 的子控件存在,此时虽然我们画面上是有一个 Flutter 的红色 `RE` 文本控件 ,不过因为是由 Flutter 直接在 Surface 直接绘制,所以 Layout Inspector 看不到只显示黑色。 + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment.center, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + + + +![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image5.png) + +此时我们添加一个通过 *Hybrid Composition* 实现一个原生的 `TextView` 控件,通过 `PlatformView` 在 Flutter 上渲染出一个灰色 `RE` 文本。 + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.6, -0.6), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment.center, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + +![image-20220308110032575](http://img.cdn.guoshuyu.cn/20220309_DWZB/image6.png) + +可以看到,如上图所示,在我们的显示布局边界上可以清晰看到它的信息: + +> `TextView` 通过 `FlutterMutatorView` 被添加到 `FlutterView` 上被直接显示出来。 + +**所以 `TextView` 是直接在原生代码上被 add 到 `FlutterView` 上,而不是提取纹理**,另外可以看到,左侧栏里多了一个 `FlutterImageView` ,并且之前看不到的 Flutter 控件红色 `RE` 文本也出现了,背景也变成了 Flutter 上的白色。 + +我们先暂时忽略新出现的 `FlutterImageView` 和 Flutter UI 能够出现在 Layout Inspector 的原因,留到后面再来分析,此时我们再新增加以一个红色的 Flutter `RE` 控件到` Stack` 里,位于 `PlatformView` 的灰色 `RE` 下。 + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.4, -0.4), + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ), + Align( + alignment: Alignment(-0.6, -0.6), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment.center, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + + + +![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image7.png) + +如上图所示,可以看到布局和渲染效果正常,Flutter 的红色 `RE ` 被上面的 `PlatformView` 灰色 `RE` 遮挡了部分,这是符合代码的渲染效果。 + +如果这时候我们把新增加的第二个红色 `RE` 放到灰色 `PlatformView` 灰色 `RE` 上,会发生什么情况? + +```dart + Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.6, -0.6), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(-0.4, -0.4), + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ), + Align( + alignment: Alignment.center, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + +![image-20220308110325717](http://img.cdn.guoshuyu.cn/20220309_DWZB/image8.png) + +可以看到红色的 `RE` 成功被渲染到灰色 `RE` 上 ,而之所以能够渲染上去的原因,是因为这个和 `PlatformView` 有交集的 `Text` ,被渲染到一个新增的 `FlutterImageView` 控件上, 也就是 Flutter 判断了此时新红色 `RE` 文本需要渲染到 `PlatformView` 上,所以添加了一个 `FlutterImageView` 用于承载这部分渲染内容。 + +如果这时候挪动第二个红色的 `RE` 让它和 `PlatformView` 没有交集,但是还是在 `Stack` 里位于 `PlatformView` 之上会如何? + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.6, -0.6), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(-0.8, -0.8), + child: new Text( + "RE", + style: TextStyle(fontSize: 50, color: Colors.red), + ), + ), + Align( + alignment: Alignment.center, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + + + +![image-20220308110754283](http://img.cdn.guoshuyu.cn/20220309_DWZB/image9.png) + +可以看到虽然 `FlutterImageView` 没了,第二个红色的 `RE` 也回到了默认的 Surface上,所以这就是 *Hybrid Composition* 混合原生控件的最基础设计理念: + +- **直接把原生控件添加到 `FlutterView` 之上**; +- **原生和 Flutter 控件混合堆叠时,用新的 `FlutterImageView` 来实现层级覆盖;** +- **如果没有交集就不需要新的 `FlutterImageView`;** + + + +关于 `FlutterImageView` 后面再展开,我们继续这个例子,让两个 Flutter 的红色 `RE` 都渲染到 `PlatformView` 的灰色的`RE `上会是什么情况? + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(0.6, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0.6, 0), + child: new Text( + "RE", + style: TextStyle(fontSize: 50, color: Colors.red), + ), + ), + Align( + alignment: Alignment.center, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + + + +![image-20220308181045237](http://img.cdn.guoshuyu.cn/20220309_DWZB/image10.png) + +如上图所示,可以看到两个红色的 Flutter `RE` 控件共享了一个 `FlutterImageView` ,这里可以得到一个新的结论:**和 `PlatformView` 有交集的同层级 Flutter 控件会同享同一个 `FlutterImageView` 。 ** + +我们继续调整示例,如下代码我们新增多一个 `PlatformView` 的灰色 `RE` 控件,然后调整位置,但是 Flutter 控件都在一个层级上,运行之后可以看到,只要 Flutter 控件都在同一个层级,就同享同一个 `FlutterImageView` 。 + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.2, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0.2, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0, -0.1), + child: new Text( + "RE", + style: TextStyle(fontSize: 50, color: Colors.red), + ), + ), + Align( + alignment: Alignment(0, 0.2), + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + +![image-20220308181925410](http://img.cdn.guoshuyu.cn/20220309_DWZB/image11.png) + + + +但是如果不在一个层级呢?我们调整两个灰色 `RE` 的位置,让 `PlatformView` 的灰色 `RE` 和 Flutter 的红色 `RE` 交替出现。 + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.2, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0, -0.1), + child: new Text( + "RE", + style: TextStyle(fontSize: 50, color: Colors.red), + ), + ), + Align( + alignment: Alignment(0.2, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0, 0.2), + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ) + ], +) +``` + +![image-20220308182349443](http://img.cdn.guoshuyu.cn/20220309_DWZB/image12.png) + +可以看到,两个红色的 Flutter `RE` 控件都单独被渲染都一个 `FlutterImageView` 上,所以我们有新的结论:**和 `PlatformView` 有交集的 Flutter 控件如果在不同层级,就需要不同的 `FlutterImageView` 来承载。** + +所以一般在使用 `PlatformView` 的场景上,不建议有过多的层级堆叠或者过于复杂的 UI 场景。 + + + +接着我们继续测试,还记得前面说过 *Virtual Display* 上关于触摸事件的问题,所以此时我们直接给 `PlatformView` 的 灰色 `RE` 在原生层添加点击事件弹出 Toast 测试。 + +```dart + Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.7, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0.8, 0), + child: new Text( + "RE", + style: TextStyle(fontSize: 50, color: Colors.red), + ), + ), + Align( + alignment: Alignment.center, + child: Container( + color: Colors.amber, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ), + ), + ], +) +``` + +![image-20220308112613726](http://img.cdn.guoshuyu.cn/20220309_DWZB/image13.png) + + + +可以看到运行后点击能够正常弹出 Toast ,所以对于 `PlatformView` 来说本身的点击和触摸是可以正常保留,然后我们调整下红色大 `RE` 和灰色 `RE` 让他们产生交集,同时给红色的大 `RE` 也添加点击事件,弹出 `SnackBar` 。 + +```dart +Stack( + fit: StackFit.expand, + children: [ + Align( + alignment: Alignment(-0.3, 0), + child: SizedBox( + height: 100, + width: 100, + child: NativeView(), + ), + ), + Align( + alignment: Alignment(0.8, 0), + child: new Text( + "RE", + style: TextStyle(fontSize: 50, color: Colors.red), + ), + ), + Align( + alignment: Alignment.center, + child: InkWell( + onTap: () { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: new Text("Re Click"))); + }, + child: Container( + color: Colors.amber, + child: new Text( + "RE", + style: TextStyle(fontSize: 100, color: Colors.red), + ), + ), + ), + ), + ], +) +``` + + + +![](http://img.cdn.guoshuyu.cn/20220309_DWZB/image14.png) + +运行之后可以看到,点击没有被覆盖的灰色部分,还是可以弹出 Toast ,点击红色 `RE` 和灰色 `RE` 的交集处,可以正常弹出 `SnackBar`。 + +所以可以看到 ***Hybrid Composition* 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生 角度,它就是原生层面的物理堆叠**。 + +现在大家应该大致对于 *Hybrid Composition* 有了一定理解,那回到前面那个一开始 Layout InSpector 黑屏 ,后来又能渲染出界面的原因,这就和首次添加 Hybrid Composition 时多出来的 `FlutterImageView` 有关系。 + + + +如下图所示,可以看到此时原生的灰色 `RE` 和 Flutter 的红色 `RE` 是没有交集的,为什么会多出来一个 `FlutterImageView` 呢? + +![image-20220309093557122](http://img.cdn.guoshuyu.cn/20220309_DWZB/image15.png) + + + +这就需要说到 `flutterView.convertToImageView()` 这个方法。 + +在 Flutter 渲染 *Hybrid Composition* 的 `PlatformView` 时,会有一个 `flutterView.convertToImageView()` 的操作,这个操作是:**把默认的 `FlutterSurfaceView` 渲染切换到 `FlutterImageView` 上** ,所以此时会有一个 新增的 `FlutterImageView` 出现在 `FlutterSurfaceView` 之上。 + +> 为什么需要 `FlutterImageView` ?那就要先理解下 `FlutterImageView` 是如何工作的,因为在前面我们说过,和 `PlatformView` 有交集的时候 Flutter 的控件也会被渲染到 `FlutterImageView` 上。 + +`FlutterImageView` 本身是一个原生的 Android View 控件,它的内部有几个关键对象: + +- `imageReader` :提供一个 surface ,并且能够直接访问到 surface 里的图像数据; +- `flutterRenderer` : 外部传入的 Flutter 渲染类,这里用于切换/提供 Flutter Engine 里的渲染所需 surface ; +- `currentImage` : 从 `imageReader` 里提取出来的 `Image` 画面; +- `currentBitmap` :将 `Image` 转为 `Bitmap` ,用于 `onDraw` 时绘制; + + + +所以简单地说 `FlutterImageView` 工作机制就是:**通过 `imageReader` 提供 surface 给 Engine 渲染,然后把 `imageReader` 里的画面提取出来,渲染到 `FlutterImageView` 的 `onDraw` 上**。 + + + +所以回归到前面的 `flutterView.convertToImageView()` ,在 Flutter 渲染 *Hybrid Composition* 的 `PlatformView` 时,会先把自己也变成了一个 `FlutterImageView` ,然后进入新的渲染流程: + +- Flutter 在 `onEndFrame` 时,也就是每帧结束时,会判断当前界面是否还有 `PlatformView `,如果没有就会切换会默认的 `FlutterSurfaceView` ; +- 如果还存在 `PlatformView ` ,就会调用 `acquireLatestImage` 去获取当前 `imageReader` 里的画面,得到新的 `currentBitmap` ,然后触发 `invalidate` 。 +- `invalidate` 会导致 `FlutterSurfaceView` 执行 `onDraw` ,从而把 `currentBitmap` 里的内容绘制出来。 + +![image-20220308171209135](http://img.cdn.guoshuyu.cn/20220309_DWZB/image16.png) + + + +所以我们搞清楚了 `FlutterImageView` 的作用,也搞清楚了为什么有了 *Hybrid Composition* 的 `PlatformView ` 之后,在 Android Studio 的 Layout Inspector 里可以看到 Flutter 控件的原因: + + + +> **因为有 *Hybrid Composition* 之后, `FlutterSurfaceView` 变成了 `FlutterImageView` ,而 `FlutterImageView` 绘制是通过 `onDraw` ,所以可以在 Layout Inspector 里出现。** + + + +那为什么会有把 `FlutterSurfaceView` 变成了 `FlutterImageView` 这样的操作?**原因其实是为了更好的动画同步和渲染效果**。 + +因为前面说过,*Hybrid Composition* 是直接把添加到 `FlutterView` 上面,所以走的还是原生的渲染流程和时机,而这时候通过把 `FlutterSurfaceView` 变成了 `FlutterImageView` ,也就是把 Flutter 控件渲染也同步到原生的 `OnDraw` 上,这样对于画面同步会更好。 + +那有人就要说了,我就不喜欢 `FlutterImageView` 的实现,有没有办法不在使用 *Hybrid Composition* 时把 `FlutterSurfaceView` 变成了 `FlutterImageView` 呢? + +有的,官方在 `PlatformViewsService` 内提供了对应的设置支持: + +```dart + PlatformViewsService.synchronizeToNativeViewHierarchy(false); +``` + +在设置为 false 之后,可以看到只有 *Hybrid Composition* 的 `PlatformView` 的内容才能在 Layout Inspector 上看到,而 `FlutterSurfaceView` 看起来就是黑色空白。 + +![image-20220308151751781](http://img.cdn.guoshuyu.cn/20220309_DWZB/image17.png) + +![image-20220308151856383](http://img.cdn.guoshuyu.cn/20220309_DWZB/image18.png) + + + +### 问题 + +那 *Hybrid Composition* 就是完美吗? 肯定不是,事实上 *Hybrid Composition* 也有很多小问题,其中就比如性能问题。 + +例如在不使用 *Hybrid Composition* 的情况下,Flutter App 中 UI 是在特定的光栅线程运行,所以 Flutter 上 App 本身的主线程很少受到阻塞。 + +**但是在 *Hybrid Composition* 下,Flutter UI 会由平台的 `onDraw` 绘制,这可能会导致一定程度上需要消耗平台性能和占用通信的开销**。 + +例如在 Android 10 之前, *Hybrid Composition* 需要将内存中的每个 Flutter 绘制的帧数据复制到主内存,之后再从 GPU 渲染复制回来 ,所以也会导致 *Hybrid Composition* 在 Android 10 之前的性能表现更差,例如在滚动列表里每个 Item 嵌套一个 *Hybrid Composition* 的 `PlatformView` 。 + +> 具体体现在 ImageReader 创建时,大于 29 的可以使用 `HardwareBuffer` ,而`HardwareBuffer` 允许在不同的应用程序进程之间共享缓冲区,通过 `HardwareBuffers` 可以映射各种硬件系统的可访问 memory,例如 GPU。 + +![image-20220309153648439](http://img.cdn.guoshuyu.cn/20220309_DWZB/image19.png) + +**所以如果当 Flutter 出现动画卡顿时,或者你就应该考虑使用 *Virtual Display* 或者禁止 `FlutterSurfaceView` 变成了 `FlutterImageView`**。 + +> 事实上 *Virtual Display* 的性能也不好,因为它的每个像素都需要通过额外的中间图形缓冲区。 + + + +## 未来变化 + +在目前 master 的 [#31198](https://github.com/flutter/engine/pull/31198) 这个合并上,提出了新的实现方式用于替代现有的 *Virtual Display* 。 + +这个还未发布到正式本的调整上, *Hybrid Composition* 基本没有变化,主要是调整了一些命名,主要逻辑还是在于 `createForTextureLayer` ,目前还无法保证它后续的进展,目前还有 一部分进度在 [#97628](https://github.com/flutter/flutter/pull/97628) ,所以先简单介绍下它的情况。 + +在这个新的实现上,*Virtual Display* 的逻辑变成了 `PlatformViewWrapper ` , `PlatformViewWrapper ` 本身是一个 `FrameLayout` ,同样是 `flutterView.addView(); ` ,基本逻辑和 *Hybrid Composition* 很像,只不过现在添加的是 `PlatformViewWrapper ` 。 + +![image-20220309163231386](http://img.cdn.guoshuyu.cn/20220309_DWZB/image20.png) + + + +在这里 *Virtual Display* 没有了,原本 *Virtual Display* 创建的 Surface 被设置到 `PlatformViewWrapper ` 里面。 + +简单介绍下:**在 `PlatformViewWrapper ` 里,会通过 `surface.lockHardwareCanvas();` 获取到当前 ` Surface` 的 `Canvas` ,并且通过 `draw(surfaceCanvas)` 传递给了 `child `**。 + +所以 `child ` 的 UI 就被绘制到传入的 ` Surface` 上,而 Flutter Engine 根据 ` Surface` 的 id 又可以获取到对应的数据,通过一个不可视的 `PlatformViewWrapper` 完成了绘制切换而无需使用 `VirtualDisplay` 。 + +当然,目前在测试中接收到的反馈里有还不如以前的性能好,所以后续会如何调整还是需要看测试结果。 + + + +> PS ,如果这个修改正式发布,可能 Flutter 的 Android miniSDK 版本就需要到 23 起步了。因为 `lockHardwareCanvas()` 需要 23 起,而不用兼容更低平台的原因是 `lockCanvas()` 属于 CPU copy ,性能上会慢很多 + +![](http://img.cdn.guoshuyu.cn/WechatIMG230.jpeg) \ No newline at end of file diff --git a/Flutter-Extended.md b/Flutter-Extended.md new file mode 100644 index 0000000..09623a0 --- /dev/null +++ b/Flutter-Extended.md @@ -0,0 +1,205 @@ +# Google I/O Extended | Flutter 游戏和全平台正式版支持下 Flutter 的现状 + +Hello,大家好,我是《Flutter开发实战详解》的作者,Github GSY 系列项目的负责人郭树煜,本次 Google I/O Extended 我主要是给大家回顾一下本次 I/O 大会关于 Flutter 的一些亮点。 + +> 其实本次 I/O 大会对我来说也有特别的意义,因为本次 I/O 大会之后,**我参加了 Dart/Flutter GDE 的最后一轮面试,有幸顺利通过了**,这对于我个人来说也是一个里程碑。 - [《从台下到台上,我成为 GDE(谷歌开发者专家) 的经验分享》](https://juejin.cn/post/7102242694755254279) + +## 游戏 + +如果要说本次 I/O 里 Flutter 有什么亮点,那其中之一必定就是官方的 Flutter 小游戏 [pinball](https://pinball.flutter.dev/#/) 。 + +![image-20220525145827978](http://img.cdn.guoshuyu.cn/20220528_未命名/image1.png) + +其实这不是第一次 Flutter 和游戏领域有交集,例如: + +- Unity 就有 Flutter 相关的 [UIWidgets](https://github.com/Unity-Technologies/com.unity.uiwidgets) ,它是 Unity 编辑器的一个插件包,可帮助开发人员通过 Unity 引擎来创建、调试和部署高效的跨平台应用; +- 腾讯的 PUBG 吃鸡游戏,其中一些游戏的非游戏 UI 已经开始转向 Flutter ; + +因为 Flutter 拥有平台无关的渲染引擎 Skia ,而 Skia 的 2D 渲染能力从手机端、Web 端到 PC 端的支持,经过这么多年的发展已经很成熟,**所以在一定程度上,Flutter 本身就是一个 2D 版的“游戏引擎”** 。 + +**Flutter 其实一直有针对游戏引擎有一个关于游戏的 [Toolkit](https://flutter.dev/games)** ,一般情况下我们可以把游戏分为两类: + +- 射击游戏、赛车游戏等的动作游戏; +- 棋盘游戏、卡牌游戏、拼图、策略游戏等休闲游戏; + +而其实上述这些休闲游戏和 App 十分接近,所以从场景上,它挺更适合使用 Flutter 来进行开发。 + +甚至在官方的 ToolKit 里,还包含了如`google_mobile_ads`, `in_app_purchase`, `audioplayers`, `crashlytics`, 和`games_services` 等工具包,**提前为广告和应用内购进行和内置集成支持**。 + +![image-20220525112604487](http://img.cdn.guoshuyu.cn/20220528_未命名/image2.png) + +当然,如果你需要实现更复杂的游戏场景,例如 [pinball](https://pinball.flutter.dev/#/) 这样的游戏效果,那么你可能就需要第三方的 [Flame ](https://pub.dev/packages/flame)包来完成,这里 GIF 有些掉帧,但是实际使用过程中,如果我不说,你不会发现这是一个 Flutter Web 写的游戏。 + +![TT](http://img.cdn.guoshuyu.cn/20220528_未命名/image3.gif) + +**Pinball 本身是基于 Flame SDK ,通过 Flutter 和 Firebase 开发的一个具备完成功能的弹珠游戏**。 + +**其中 Flame 提供了各类游戏相关的开箱即用功能,例如动画、物理、碰撞检测等**,同时 Flame 还可以利用了 Flutter framework 的基础内容,所以如果你是 Flutter 的开发者,那么其实你已经具备使用 Flame 构建游戏所需的基础。 + +> 其实 Flame 仓库创建于在 2017,并且此之前也有一些使用 Flame 开发的样例子,只是这次 I/O 官方通过 Pinball 游戏,给 Flame 做了一些背书。 + +在官方的例子就提供了游戏里关于 Camera 的相关示例,在点击屏幕时会添加一个比萨,摄像头会跟随移动,另外在这个例子中还有一些多米诺牌排列在一起,在它会和比萨产生碰撞,从而使瓷砖倾斜,并且引起一些列的物理连锁反应。 + +![rrr](http://img.cdn.guoshuyu.cn/20220528_未命名/image4.gif) + +```dart +class CameraExample extends DominoExample { + static const String description = ''' + This example showcases the possibility to follow BodyComponents with the + camera. When the screen is tapped a pizza is added, which the camera will + follow. Other than that it is the same as the domino example. + '''; + + @override + void onTapDown(TapDownInfo details) { + final position = details.eventPosition.game; + final pizza = Pizza(position); + add(pizza); + pizza.mounted.whenComplete(() => camera.followBodyComponent(pizza)); + } +} +``` + +另外,其实在 2020 年也有一些开发者使用Flutter&Flame在游戏上进行实践,例如掘金上的 [吉哈达](https://juejin.cn/post/6857049079000760334) 在 2020 年就发布过基于 Flame 的坦克大战游戏,本身也是一个比较完整的开源小游戏。 + +![](http://img.cdn.guoshuyu.cn/20220528_未命名/image5.webp) + +回到 Pinball ,如果你去看 Pinball 游戏的代码,你就会发现它使用的是 Flutter Web 里的 CanvasKit 作为渲染,也就是通过 WebAssembly + Skia 实现的绘制。 + +![image-20220525101651021](http://img.cdn.guoshuyu.cn/20220528_未命名/image6.png) + +了解过 Flutter 的同学可能知道,Flutter Web 默认在 PC 使用 CanvasKit 渲染 UI ,而在手机端默认会使用 Html 来绘制 UI ,但是如果你使用了 Flame ,那么在手机端也会是 CanvasKit ,**因为从设计上考虑,只有 CanvasKit 更符合游戏的设计思想和保持运行效果的一致性**。 + +![image-20220525102914789](http://img.cdn.guoshuyu.cn/20220528_未命名/image7.png) + +当然,这也带来了加载太慢的问题,可以看到打开 pinball 大概花费了 3.6 min,这确实是 Flutter Web 在 CanvasKit 下的通病之一。 + +而 Flutter 开发游戏和在传统 App 中不同的点主要在: + +- 一般传统 App 通常屏幕在视觉上是静态的,直到有来自用户的事件或交互才会发生变化; +- 对于游戏这一情况正好相反——UI 需要不断更新,游戏状态会不断发生变化; + +所以 在 I/O Pinball 中,游戏通过 loop 循环对球在赛场上的位置和状态做出反应,例如球与物体发生碰撞或球脱离比赛,从而做出相应。 + +```dart +@override +void update(double dt) { + super.update(dt); final direction = -parent.body.linearVelocity.normalized(); + angle = math.atan2(direction.x, -direction.y); + size = (_textureSize / 45) * + parent.body.fixtures.first.shape.radius; +} +``` + +另外还有,在构建 I/O Pinball 下,可以看到界面是有明显的类 3D 效果,那如何仅使用 2D 元素创建 3D 效果? + +![image-20220525152706873](http://img.cdn.guoshuyu.cn/20220528_未命名/image8.png) + +**其实就是通过对组件进行排序和堆叠资源的层级,以此来以确定它们在屏幕上的呈现位置**,例如当球在斜坡上发射时,球的所在的层级顺序增加,因此它看起来在斜坡的顶部。 + +```dart +/// Scales the ball's body and sprite according to its position on the board. +class BallScalingBehavior extends Component with ParentIsA { + @override + void update(double dt) { + super.update(dt); + final boardHeight = BoardDimensions.bounds.height; + const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor; final standardizedYPosition = parent.body.position.y + (boardHeight / 2); + final scaleFactor = maxShrinkValue + + ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;final ballSprite = parent.descendants().whereType(); + if (ballSprite.isNotEmpty) { + ballSprite.single.scale.setValues( + scaleFactor, + scaleFactor, + ); + } + } +} +``` + +另外弹球游戏场上有一些元素,如 Android、Dash、Sparky 和 Chrome Dino,它们都是有动画效果。 + +对于这些使用的是 sprite sheets,它包含在带有 `SpriteAnimationComponent` ,对于每个元素都有一个文件,其中包含不同方向的图像、文件中的帧数以及帧之间的时间。 + +使用这些数据,`SpriteAnimationComponent` 在 Flame 内将所有图像循环编译在一起,从而使元素看起来具有动画效果。 + +![image-20220525115124190](http://img.cdn.guoshuyu.cn/20220528_未命名/image9.png) + +```dart +final spriteSheet = gameRef.images.fromCache( + Assets.images.android.spaceship.animatronic.keyName, +);const amountPerRow = 18; +const amountPerColumn = 4; +final textureSize = Vector2( + spriteSheet.width / amountPerRow, + spriteSheet.height / amountPerColumn, +); +size = textureSize / 10;animation = SpriteAnimation.fromFrameData( + spriteSheet, + SpriteAnimationData.sequenced( + amount: amountPerRow * amountPerColumn, + amountPerRow: amountPerRow, + stepTime: 1 / 24, + textureSize: textureSize, + ), +); +``` + +最后 Flame 代码库还附带一个组件沙箱,类似于 UI 组件库,可以在开发游戏时,这是一个有用的工具,因为它允许开发者单独开发游戏组件,并确保它们在将它们集成到游戏中之前的外观和行为符合预期。 + +![1*zAjKICKgCTiEiiMTou9MJQ](http://img.cdn.guoshuyu.cn/20220528_未命名/image10.gif) + + + + + + + +## 全平台 + +Flutter 3.0 另外一个重点就是**增加了对 macOS 和 Linux 应用程序的稳定支持,这是 Flutter 的一个里程碑,现在借助 Flutter 3.0,开发者可以通过一个代码库为六个平台构建应用**。 + +![image-20220525115916985](http://img.cdn.guoshuyu.cn/20220528_未命名/image11.png) + + + +自此 Flutter 终于全平台 stable 支持了,这种支持不是说添加对应平台的UI 渲染致支持就可以:**它包括新的输入和交互模型、编译和构建支持、accessibility 和国际化以及特定于平台的集成等等,Flutter 团队的目标是让开发者能够灵活地利用底层操作系统,同时根据开发者的选择尽可能多的共享 UI 和逻辑**。 + +> 例如在 macOS 上,现在支持 Intel 和 Apple Silicon,提供 [Universal Binary](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fapple-silicon%2Fbuilding-a-universal-macos-binary) 支持,允许应用打包支持两种架构上的可执行文件,Flutter 利用了 [Dart 对 Apple 芯片的支持](https://link.juejin.cn/?target=https%3A%2F%2Fmedium.com%2Fdartlang%2Fannouncing-dart-2-14-b48b9bb2fb67) 在基于 M1 的设备上更快地编译并支持 macOS 应用程序的 [Universal Binary](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fapple-silicon%2Fbuilding-a-universal-macos-binary) 文件。 + +本次 I/O 官方就提供了一个 Flutter 合作伙伴的案例:[Superlist](https://link.juejin.cn/?target=https%3A%2F%2Fsuperlist.com%2F) ,它是 Flutter 如何实现 Desktop 应用的一个很好的例子,它在 I/O 当天发布了测试版。 + +![RR](http://img.cdn.guoshuyu.cn/20220528_未命名/image12.gif) + +Superlist 将列表、任务和自由格式内容,组合成全新的待办事项列表和个人计划,提供协作能力,同时 Superlist 也是开源项目 [super_editor](https://github.com/superlistapp/super_editor) 的维护组织,所以社区的支持其实对于 Flutter 来说很重要。 + +**每个 Flutter 正式版的发布都包含了大量来自社区的 PR ,例如本次 Flutter 3.0 版本发布就合并了 5248 个 PR**。 + +**当然,本次在 PC 端还有做了一定的取舍:放弃 Windows 7/8**。 + +在 Flutter 3.0 中推荐将 Windows 的版本提升到 Windows 10,虽然目前 Flutter 团队不会阻止在旧版本(Windows 7、Windows 8、Windows 8.1)上进行开发,但 [Microsoft 不再支持](https://link.juejin.cn/?target=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Flifecycle%2Ffaq%2Fwindows) 这些版本,虽然 Flutter 团队将继续为旧版本提供“尽力而为”的支持,但还是鼓励开发者升级。 + +> **注意**:目前还会继续为在 Windows 7 和 Windows 8 上能够正常*运行* Flutter 提供支持;此更改仅影响开发环境。 + +另外,Flutter 在 PC 领域虽然目前不像 App 端那么丰富,但是社区也涌向了一批优质的第三方支持,例如 [leanflutter.org](https://github.com/leanflutter) 目前发布了很多关于 PC 端相关的内容,大家可以在 pub 或者 github 看到相关的内容,其中比如 + +- **window_manger 就在 PC 领域备受关注**,它本身是用于调整窗口的桌面应用的大小和位置,支持 macOS、Linux、WIndows等平台,所以这个包在桌面端领域就相当实用; +- flutter_distributor 可以帮助你在多个平台上实现自动构建和定制化的发布 + +![image-20220528215030023](http://img.cdn.guoshuyu.cn/20220528_未命名/image13.png) + +**类似 leanflutter 等作者已经在 Pub 发布了很多关于 PC 端能力拓展的插件**,所以大家对于 PC 端支持的忧虑可以开始放下,尝试一些 Flutter 的 PC 端开发。 + +> **注意是 leanflutter 不是 learnflutter**。 + +最后,目前 Flutter PC 端在国内也开始被越来越多的大厂所接纳,比如知名的钉钉、字节、企业微信都在 Flutter PC 端进行投入开发,它们的投入使用也可以反向推动 Flutter PC 端的健康成长。![image-20220525135033045](http://img.cdn.guoshuyu.cn/20220528_未命名/image14.png) + +就比如官方的 2022 roadmap 提到:**无论一个 SDK 有多么优秀,如果只有少数人在使用它,它都不能反映出它的价值; 而如果 SDK 很普通但是却被大量开发人员使用,它也会有一个健康和有价值的框架,使用这个框架的人才能真正从社区和框架中受益**。 + +![image-20220528215500266](http://img.cdn.guoshuyu.cn/20220528_未命名/image15.png) + + + + + +dff \ No newline at end of file diff --git a/Flutter-FF.md b/Flutter-FF.md new file mode 100644 index 0000000..bd7c0cb --- /dev/null +++ b/Flutter-FF.md @@ -0,0 +1,439 @@ +# Flutter Festival | 2022 年 Flutter 适合我吗?Flutter VS Other 量化对比 + + +Hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 系列开源项目的负责人郭树煜,比如 [gsy_github_app_flutter](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FCarGuo%2Fgsy_github_app_flutter) 、GSYVideoPlayer 等的项目 。 + +> 看到这个题目大家应该知道,今天这个主题并不是纯粹的技术内容分享,可以说还有点吃力不讨好,其实我很少分享这类主题,不过最近觉得有必要做这么一个算是科普向的内容吧。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image1) + + + +## Flutter 的现状 + +我是在 2017 年左右接触的 Flutter ,说来起来有趣,那时候因为我需要做一场关于跨平台技术的内部分享,主要目的是给公司其他事业部推 React Native 框架,好巧不巧地那时候刚好看到 Flutter ,就被我当作凑数的“添头”给加到分享里,自此我就开始了和 Flutter 之间的故事。 + +回到正题,Flutter 开源至今其实已经将近 7 年的时间,如今在 2022 年看来,**Flutter 已经是不再是以前小众的跨平台框架了**。 + +![image-20220222115737486](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image2) + +如图所示,截止我 2 月份截图时,可以看到如今的 Flutter 已经有高达 `137k` 的 star , `10k+` Open 和 `50k+` Closed 的 issue 也足以说明 Flutter 社区和用户的活跃度。 + +**从官方公布的数据上, Flutter 已经基本超过其他跨平台框架,成为最受欢迎的移动端跨平台开发工具,截至 2022 年 2 月,有近 50 万个应用程序使用了 Flutter**。 + +如图所示,去年下半旬的数据调查中,**Flutter 也成为了排名第一的“被使用”和“被喜爱”的跨平台框架**,可以看到 Flutter 在 2019 到 2022 有了很明显的增长,有接近 42% 的跨平台开发者会使用 Flutter。 + + +![image-20220222115623672](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image3) + +![image-20220222115549701](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image4) + + + +其实在去年和前年,我也做过一些简单的统计: + +- 2020 年 `52` 个样本中有 `19` 个 App 里出现了 Flutter; +- 2021 年 `46` 个样本中有 `24` 个 App 里出现了 Flutter; + +本次基于 2022 年 2 月 22 号,在对比了 **57 款**常用 App 之后得到的数据: + +| Flutter | React Native | Weex | 没有使用跨平台 | +| :----------------------------------------------------------- | :----------------------------------------------------------- | :------------------------------------------------- | ------------------------------------------------------------ | +| 27 | 24 | 5 | 13 | +| 链家、转转、掘金、**中国大学 MOOC**、同花顺、饿了么、凤凰新闻、微信、微视、哔哩哔哩漫画、腾讯课堂、企业微信、学习强国、闲鱼、携程旅行、腾讯会议、**微博**、贝壳找房、百度网盘、 WPS Office、唯品会、**美团众包**、**美团外卖商家版**、**UC**、QQ(libmxflutter),**小米运动**、**优酷视频** | 美团 、**美团众包** 、**美团外卖商家版** 、美团外卖、爱奇艺、**中国大学 MOOC** 、脉脉 、小红书、安居客、得物、58同城、微信读书、汽车之家、飞书、喜马拉雅、去哪儿旅行、菜鸟、京东、快手、携程、**米家**、**UC**、**小米运动**、**优酷视频** | **UC** 、**闲鱼** 、 **微博** 、**米家、优酷视频** | QQ音乐、Boss直聘、今日头条、流利说、知乎、腾讯新闻、财经社、酷狗音乐、拼多多、抖音、起点、什么值得买、百度地图 | + +> 这些数据来源于 Android 的 Apk ,以是否存在` libflutter.so` 、`libreactnativejni.so` 和 `libweexcore.so` 等动态库为依据,如果项目使用了插件化下发可能会被忽略。 + +可以看到 Flutter 和 React Native 的出现都接近 50%,而 Weex 的占有率已经很低,**另外在这个小样本下,可以看到现在大多数 App 或多或少都可能带有一些跨平台框架的趋势**。 + +> 同时,加粗部分的 App 因为业务需要, 在应用内使用了不止一种的跨平台框架,比如UC、闲鱼等。 + +而在官方去年的 Q4 数据调查里,*在过去 6 个月中,分别**有 72% 和 91% 的开发者使用 Flutter 为 iOS 和 Android 开发 App*** 。 + +![0*TERDonM4zc_kafRm](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image5) + + + +再看一份数据,是 Dart 的第三方插件托管平台 pub.dev 上的数据,基于 2022-02-22 的数据: + +| All | 23495 packages | +| ----------- | -------------- | +| Flutter | 21714 packages | +| Android/iOS | 20352 packages | +| Web | 12584 packages | +| PC | 14314 packages | +| Null safety | 12615 packages | + +目前大概有 2.3 万个公开的第三方支持包托管在 pub 上,其中支持 Flutter 的有 2.1 万个,可以看出 Dart 语言的用户基本都是来源于 Flutter 。 + +另外从数据上看大部分的库都支持 Android 和 iOS ,而对于 Web 和 PC 的支持接近60% ,而比较意外的是,目前支持 Null safety 的包也就接近60%,也就是还有 40% 多的包还停留在较老的版本上。 + +而在官方的 Q4 调查里可以看到,**使用 Flutter 作为主要工作的比例在逐步提高**。 + +![0_Zw_zyVq5CfP7Y09o](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image6) + +最后在聊一聊 Flutter 官方对于 Flutter 一直坚持一个理念: + +**一个 `SDK` 再优秀,如果只有少部分人在使用,那它也不能体现价值;而一个 `SDK` 即使平庸,但是有大量开发者使用,那也会拥有一个健康繁荣的生态氛围,这样使用框架的人才能从中受益**。 + +> 补充一句,你知道调查里大家最不满意的 Flutter 的是哪个方面吗? +> +> **是文本编辑**!Q4调查里,对文本编辑功能的满意度从 82.3%(单行)和 82.2%(过滤和格式化)下降到 69.6%(多行)和 66.6%(富文本编辑器),目前多编辑体验和输入富文本支持上,确实不是特别友好。 + +## Flutter VS Other + +聊完 Flutter 的现状,我们继续讨论 Flutter 和其他框架的一些直观对比。 + +### 实现原理 + +这部分内容其实分享过很多次,简单说一下,首先对比它们的实现原理,如下图所示,可以看到: + +- 对于原生 Android 或者 Compose 而言,是**原生代码经过 skia 最后到 GPU 完成渲染绘制**,Android 原生系统本身自带了 skia; + +- 对于 Flutter 而言,**Dart 代码里的控件经过 skia 最后到 GPU 完成渲染绘制**,这里在 Andriod 上使用的系统的 skia ,而在 iOS 上使用的是打包到项目里的 skia ; + +- 对于 ReactNative/Weex 等类似的项目,它们是**运行在各自的 JS 引擎里面,最后通过映射为原生的控件,利用原生的渲染能力进行渲染**; + +- 对于 uni-app 等这类 Hybird 的跨平台框架,使用的主要就是 **WebView 的渲染能力**;(不讨论开启weex情况) + + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image7) + + + +首先看到,从理论上来说, **Flutter 在实现上是最接近原生,因为从实现路径上基本是一致的,而 RN/Weex 相对会差一些,而 uni-app 通过 WebView 的渲染会是最末**。 + +但是对于性能问题,**事实上很多时候性能门槛不在于框架,而在于开发者**,我见过用 Cordova 开发的 App 性能和体验都调教得很不错,我记得有一次大会分享和支付宝的大佬聊过,支付宝也使用了很多 H5 的 Hybird 技术,得益于 UC 的自研内核,在性能体验上一直还挺不错。 + +### 构建大小 + +接着我们对比应用构建的大小,这里主要对比 Android ,因为 iOS 上应用的大小似乎越来越没人在意,比如 QQ 这个极端的例子: + +![image-20220225113714959](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image8) + + + +回到问题上,关于应用大小问题,之前恰好看到有多人说过: + +> “Compose 上 Kotlin/JVM 为 JVM 和 Android 平台生成 jar/aar 文件、通过 Kotlin/Native 为 IOS 平台生成 framework 文件、通过 Kotlin/JS 为 Web 平台生成 JavaScript 文件,最终调用的还是原生 API,这使得采用 Compose Multiplatform 不会导致性能损耗,且不会像 Flutter 那样明显增大应用体积。” + +是的,从实现上看 Flutter 在实现上确实应该比 Compose 占据更多体积,但是真实情况是怎么样呢? + +首先我们创建几个空项目,然后打包时只保留 `arm64-v8a` 相关的动态库,因为一般情况下上架也只会保留其中一种 so 库。 + +在我们不写任何代码的情况下,构建出 Android 的 Release 包,得到如下结果: + +- Flutter + +![l37e90013d3143b59b2fedac8175846c2-s-mab43156a06c705c0e724893593dff285](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image9) + +- React Native + +![l86641a7d82e2feff1f984855ecbd562c-s-mdb88514a3334653b9e61c27c51634605](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image10) + +- Compose + +![l6125476d649868bb69a29a009574a232-s-mf07265732fba4b70ab8330b8014db858](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image11) + +- 原生 Android + +![l64e110c95184cd1d58dc061c7a37337f-s-m7042d4089e4705f94ae59f9477189827](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image12) + + + +可以看到 : + +- React Native 的空包最大,主要体积来自于其内部的各种动态库,比如 JSCore ; +- Flutter 次之,主要体积来也是自于其内部的动态库,比如 Flutter 的 framework; +- Compose 的体积和原生相当接近,主要内容来自于 classes 文件;当然这里没有混淆和压缩,混淆和压缩后可以小很多; + + + +从结果上看空项目下确实是 Flutter 比 Compose 所占据的体积更大,但是这里有一点需要注意的是: + +- 单纯 Flutter 开发下,主要的应用体积会来自 `libapp.so` ,这部分代码是经过 AOT 编译后的 Native 二进制代码; +- 而 Compose 的体积增长主要来自于 classes 文件,这部分的代码增长需要通过混淆等来压缩; + +额外提一点,**大家可能会好奇 Compose 编译后是怎么完成布局渲染**? + +这里简单介绍下,**Compose 里的控件和原生控件并不是一个体系**,大家如果去看编译后的内容,就会发现例如 `BOX` 这样的控件在编译后是通过 `ComposerKt` 和 `BoxKt` 等的 framework 实现来完成的布局与绘制。 + +![image-20220223163739226](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image13) + +所以 **Compose 编译后不是转化为原生的 Android 上的 View 去显示**,而是依赖于平台的 `Canvas` ,在这点上和 Flutter 有点相似,简单地说可以理解为 Compose 是全新的一套 View 。 + +> 另外友情提示:虽然是全新的 View ,但是 `Compose` 的组件在 Android 上是可以显示了布局边界。 + +回顾到体积的问题上,因为我恰好开源有一些列 GSY 项目,它们实现的业务逻辑十分相似,所以都打包成 Release 模式之后,我们对比它们的体积大小: + +- Flutter + +![ld53439aaeaa21568253c98480767caee-s-m1c224ff23fb985b1bded376f0cceebdc](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image14) + +- React Native + +![lca0d0a439e3b9d18d0195521fad90c14-s-m7fdc781bd60e584b0c161115fa824f43](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image15) + +- 原生 Android + +![le2f6c0258c501a6fdae93b47deff024c-s-mc5ea88d982f7d79299b1b0391b7e95ab](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image16) + + + +因为我目前还没有 Compose 的项目,所以这里以原生作为对比,可以看到: + +- Flutter 项目从空的 5.7 M 变成了 9.8M ,增长了 4.1 M 的大小; +- React Native 项目从 9.4 M 变成了 12.7M,增长了 3.4 M 的大小; +- 原生项目从 3.2 M 变成了 9.3 M ,增长了 6.1 M 的大小; + +虽然不精准,但是可以看到在大致相同的业务场景下, **Flutter 和原生项目的总大小反而相差不大,而原生项目的增加其实比 Flutter 更显著一些**。 + +但是这里的前提是原生不开启压缩和混淆,如果开启压缩和混淆之后,如下图所示可以看到体积发生了变化,体积从 9.3M 变成了 6.4 M ,所以大致上可以看出,**在开启混淆和压缩之后,原生 App 体积增长和 Flutter 差异不会太大**。 + +![image-20220223172138586](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image17) + + + +另外,这里我找了一个网上的纯 Compose 做了测试,在开启混淆和压缩后,Compose 体积的大小变化就十分显著:**从 9.6 M 变成了 2.4 M ,这得益于 Compose 里的代码基本都是可以被混淆的**。 + +![image-20220223170138121](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image18) + +![image-20220223170242884](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image19) + + + +所以得到结论: + +- **在开启了压缩之后的 Compose ,体积确确实实会比 Flutter 更小更有优势,这里的优势来源于 classes 的压缩效率** +- **React Native 的体积一般情况下都会比 Flutter 更大,同理 Weex 也类似;** + + + +当然这个也不是绝对的,体积大小有时候也和开发者的习惯有关系,比如某天我就在群里刚好看到,某个 App 的 Flutter 业务动态库居然可以高达 77.4 M 。 + + + +![lb47319abc6776b6ac76d45775ecfa7e8-s-m0f2c227b7d605d6930401a084bd16170](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image20) + + + +这是什么概念?一般情况下 10M - 15M 就是普通中小型 App 的 Flutter 动态库大小 ,而 大型 APP 一般也会控制在 20M - 35M 之间,就算是很大的体积了,例如 UC 也就是 35 M 、企业微信 28.9M 的水平。 + + + +![lf6f59898fbe0c6b11639e81c92796155-s-m5f015f612b387f867b36de285a780d88](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image21) + + + +![le0e42c9311522a26d2a83afda916b3db-s-m1548598cff0bb0080caf643514588d90](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image22) + + + +所以体积大小上,更多是开发者的主观控制,也和你是否开启混淆和压缩有关系,主要介绍这个是让大家对不同项目的打包产物有个直观的认识,从而对选择哪种开发框架提供一个判断的依据。 + +### 构建过程 + +接下来聊聊构建过程,为什么聊这个,因为对于新手来说,构建过程的问题是一个很容易放弃的过程。 + +如下图所示就是非原生开发在运行 Flutter 时经常可以遇到的问题: + +![l9d17062e9171bfc73b423f52f22bae27-s-m908dc799447e36c1a7c8ca7275416992](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image23) + + + +如果你看到运行后一直停留在` assembleDebug` 阶段没有进入下一步,那这时候其实是 Android 在通过网络下载一些环境依赖,比如 Gradle SDK、 aar 库等这些运行所需的包,而这个过程通过 `flutter run` 或者 idea 运行是看不出来进度的,你只有进入 `andorid/` 目录下执行 `./gradlew assembleDebug` 就可以看到类似的进度: + +![l0d886e19d6ba2c3eca279cea12c62628-s-me77c94c2fa3e28791919d0d3153efc9f](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image24) + +例如在 Flutter 官方 Q4 的调查里,在发布应用程序时,需要处理 Xcode (iOS) 和 Gradle (Android) 是最常见的问题,为什么说这个? 首先这里可以看出一点,**对原生平台的不熟悉会是使用跨平台开发的一个痛点**。 + +![0_3MXILeNbFbIFagLu](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image25) + +![0_qtLcSyZO68tuY6Gk](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image26) + +当然,在对比所有跨平台开发的这个环节里, Flutter 虽然不能说是最好,但是 React Native 绝对是最拉胯的,因为不管是 Weex 还是 React Native , node_module 黑洞一直都是头痛的问题: + +![image-20220223173738326](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image27) + + + + + + + +举个例子,React Native 项目的 node_module 黑洞,经常导致了它在环境安装和运行上会给你“惊喜”,各种丰富的插件和工具,在实用的同时又成了臃肿的坑,比如这是我前段时间久违需要处理一个 React Native 项目时遇到的问题: + +![l44f7689357e4deb77b7c5019177f3442-s-m2fc075a1dd990c3aaabc19acb201f279](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image28) + +![lc29a1742b1876eea0deee1c895d05a1a-s-m1d64d4caf0ea69d9a76e350e370f46de](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image29) + +**依赖中的依赖,各种库的版本所需的 node 环境不同,需要我从中平衡出一个合适的版本**。当然这不是最麻烦的,最麻烦的是在电脑 A 上运行成功之后,在 B 电脑 npm 之后发现无法运行的问题,相信这是每个 React Native 开发的必修课。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image30) + +> 从前端开发角度,比如扁平化依赖,当然扁平化依赖的展开后依赖深度就变成了数量很可观的文件目录,依赖结构变得就不直观了,当然现在的 npm ,pnpm 工具都有了新的优化, + +相反 Flutter 在这方面就轻量很多,目前 Dart 的 pub 包层级很浅,路径相对清晰,这也是我觉得在这方面 Flutter 基本上比 React Native 更舒服的原因,**所以在原生环境依赖复杂度一致的情况下,Flutter 确实比 RN 更容易进入 hello world** 。 + + + +### Flutter & Compose + +最后聊聊 Flutter 和 Compose 之间的对比。 + +相信大家对于 Flutter 和 React Native 之间的对比看得多了,因为 React Native 发布至今已经很久了,并且 Flutter 和 React Native 之间是不同公司在维护 ,而**对于 Flutter 和 Compose ,它们都是谷歌开源的项目,并且都在支持多平台,那它们之间有什么不同?应该如何选择?** + + + +首先提一个题外话:**前端有 npm 、Flutter 有 pub 、iOS 有 cocoaPods,你可以通过它们的官网搜索你想要的库,查看它们的热度,版本,兼容和使用量等等信息,但是 Android 呢?** + +Android 的 Gradle 是不是缺少了这样一个便捷的存在,以至于我们只能在 Github 通过关键字去检索,而这个影响其实也渗透到 Compose 里,这对 Compose 在跨平台发展上是一个问题。 + +首先谷歌官方的定义,**Compose 是 Android 的现代原生界面工具包,而且正如前面我们介绍的,它是一套全新的 UI ,所以 Compose 是有自己的平台,也就是 Android,那是它的主场**。 + +> 从可以看官方的 [路线图]( https://developer.android.google.cn/jetpack/androidx/compose-roadmap) 可以看出来, 谷歌对 Compose 的经历主要都是集中在 Android 原生平台,而 Compose Multiplatform 是由 JetBrains 维护的 [compose-jb ](https://github.com/JetBrains/compose-jb ) 来实现。 + +**Flutter 没有自己的平台** ,它是一个跨多平台的 UI 框架,它出生就是为了多平台而生,从目前支持的 Android、iOS、Web 、Window 都发布了正式版支持,而 Linux 和 MacOS 估计也不远了。 + +所以这是它们直接最大的区别之一:**Compose 是谷歌为 Android 设计的全新 UI 框架,并且 JetBrains 把它拓展到支持跨平台,而 Flutter 主要就是为了跨平台而生** 。 + +虽然都支持跨平台,但是二者之间也是有很大差异,如图所示是它们实现上的不同: + +![image-20220223174643400](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image31) + + + +在实现上的差异是: **Flutter 对外是通过一套官方的 Framework 来支持多平台,而 Compose 目前是通过多个模块不同实现来支持多平台**。 + +Flutter 不用说,就是通过编译时不同的命令去生成不同平台的代码,这期间统一有 Flutter framework 来完成输出,而目前 Compose 在 Web 、Desktop 和 Mobile 上的实现逻辑是并不一定能通用的,特别是 Web。 + +> Compose 目前在 iOS 还没有正式的支持,虽然可以通过一些方式支持,但是还不是特别方便,而在 Web 上 Compose 需要使用和导入的包也是具备特殊化,反而是 Mobile 和 Desktop 之间反而是能共用 `compose-ui` 的内容。 + +举个例子,在 [compose-jb ](https://github.com/JetBrains/compose-jb ) 里 对 Web 的支持代码如下,可以看到导入的和使用的控件都具备它自己的特殊性。 + +```kotlin +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.css.* + +fun main() { + var count: Int by mutableStateOf(0) + + renderComposable(rootElementId = "root") { + Div({ style { padding(25.px) } }) { + Button(attrs = { + onClick { count -= 1 } + }) { + Text("-") + } + + Span({ style { padding(15.px) } }) { + Text("$count") + } + + Button(attrs = { + onClick { count += 1 } + }) { + Text("+") + } + } + } +} +``` + + + +所以对于 Compose 来说,更多像是:你学会了这个框架,然后就具备了写 Web 和 Desktop 的能力;而对于 Flutter 来说它在跨平台的体验会更好。 + +所以从我理解上是: + +- **Compose 是 Jetpack 系列的全新 UI 库**,主要是被应用到 Android 界面开发,它就是为了重新定义 Android 上 UI 的编写方式, 所以你也可以选择不用,用不用都能开发 Android 的 UI,**但是如果你继续在Android 上深耕,那么你最好还是要学会**。 +- **Flutter 的未来在于多平台,更稳定可靠的多平台 UI 框架。如果你的路线方向不是大前端或者多端开发者,那你可以不会也没关系。** + +而从使用这角度,不管你是先学会 Compose 还是先学会 Flutter,对于你掌握另外一项技能都有帮助,相当于学会一种就等于学会另一种的 70% : + +- **如果你是原生开发,还没接触过 Flutter , 那先去学 Compose** ,这对你的 Android 生涯更有帮助,然后再学 Flutter 也不难。 +- **如果你已经在使用或者学习 Flutter ,那么请继续深造**,不必因为担心 Compose 而停滞不前,当你掌握了 Flutter 后其实离 Compose 也不远了。 + +> 对比了 Flutter 和 Conpose 的很多设计理念和源码,他们在实现上的相似度很高。 + +当然,**跨平台之所以是跨平台,首先就是要有对应原生平台的存在,** 很多原生平台的问题都需要回归到平台去解决,那些喜欢吹 xxx 制霸原生要凉的节奏,仅仅是因为“你的焦虑会成为它们的利润”,没有了平台还要跨平台干嘛? + + + +## 一些见解 + +最后简单聊聊我的一些见解。 + +### 跨平台的底层逻辑 + +在 Flutter 之前,移动端跨平台的底层逻辑无非两种: + +- 一种是靠 WebView 跨平台; +- 一种是靠代理原生控件跨平台; + +所以早期的移动端跨平台控件一开始就 Cordova 、Ionic 等这些框架,它们的目的就是将前端 H5 的能力拓展到 App 端,让前端开发能力也可以方便开发 Android 和 iOS 应用,那时候的口号我记得是:**write Once, run everywhere** 。 + +后来,得益于 React 的盛行,React Native 开辟了新的逻辑:用前端的方式去写原生 App ,通过把 JS 控件转化为原生控件进行渲染,让移动端跨平台的性能脱离了 WebView 的限制,性能得到了提升,而 React Native 强调的是 **learn once, write everywhere** ,也就是你学会了 React ,可以开发网页,也可以开发 App 。 + +而到了 Flutter ,它直接摆脱了平台控件的依赖,它自己产出了一套平台无关的控件,通过 GPU 直接渲染出来,这样做的成本无疑是最高的,但是所带来的“解耦”和“所见即所得”无疑是最好的,而 Flutter 的口号是 **Build apps for any screen** 。 + +**但是如果是放到真实应用场景上,不是说 Flutter 就是最优解,而是需要衡量你的业务场景来选择合适你的框架** , 例如: + +- 如果你的业务场景是多框架混合开发,那 Flutter 明显不占据优势; +- 如果你的场景是需要很强的文本编辑和富文本场景,那 Flutter 明显不占据优势; +- 如果你的 KPI 对内存占用特别敏感,那 Flutter 也不是特别占据优势; +- 如果你需要热更新,那 Flutter 也并不占据优势; + +### 热更新 + +既然说到热更新,就简单介绍下热更新的问题。首先 Flutter 官方并不支持热更新,不像 React Native 一样有着十分成熟且通用的 `code-push` 框架。 + +> 为什么呢?首先 React Native 写的 JS 代码是属于纯脚本文本,就算打包成 bundle 文件它也是纯文本格式,所以通过 `code-push` 下发一个文本 bundle 并不违规,同时 `code-push` 也没办法下发打包后的原生平台代码,因为那不合规。 + +Flutter 打包后的 AOT 代码属于可执行二进制文件,如果通过热更新逻辑直接下发它,那无疑是违法了苹果 App store 和 Google Play 的政策,那 Flutter 能不能热更新呢? + +答案是可以的,鉴于国内对热更新的“必须性”,也诞生了许多第三方框架,例如: + +> MxFlutter(腾信) 、Fair (58 同城) 、 liteApp (企业微信)、Flap (MTFlutter 美团)、flutter_code_push (chimera) 等等。 + +它们都不是直接下发编译后的二进制代码,例如: + +- MxFlutter 是用 js/ts 写控件来下发更新; +- liteApp 是通过 vue 模版来输入; +- Flap 是对 Dart 的 DSL 和编码过程做处理下发; + +这些做法都需要为了热更新去做一些牺牲,所以本质上 Flutter 在热更新这个问题一直“不友好”。 + +> 当然,如果不上架 Google Play ,那么 Android 热更新 so 动态库本来就不是什么门槛,所以如果你其实可以在 Android 上粗暴地使用已有的插件化方案解决。 + +### 多平台 + +最后说一些 Flutter 的多平台,还记得前面说的 **Build apps for any screen** 吗?Flutter 不也是 *write Once, run everywhere* 吗?官方不就是支持一套代码直接打包 Android、iOS、Web、Window、MacOS、Linux 这些平台吗? + +**从我的经验出发,我想说 *write Once, run everywhere* 很美好,但是不现实**。 Flutter 确实可以一套代码直接运行到所有平台,但是就目前的体验而言,一套代码去适配所有平台的成本远远高于它所带来的便捷。 + +先说 Web ,Web 平台在几个平台里最特殊,因为它本身就需要适配 Mobile 端和 PC 端的操作逻辑,而目前Flutter Web : + +- 在 Mobile 端使用的是 `HtmlCanvas` ,也就是转化为 Web 端的“原生”控件进行渲染,这就带来了耦合和 API 适配的难度; +- 在 PC 端 Flutter 可以使用 `CanvasKit` 来进行绘制,但是它使用 `wasm` 技术目前相对“激进” ,实际无论在体积、SEO、兼容性上都存在问题; + +**所以 Flutter Web 目前还不好用,那它发布的稳定版本意义在哪里? 就在于你的代码支持打包成 Web!** + +当你在构建完关于 Android 和 iOS 的应用后,你可以把 App 的一些 UI 和业务快速构建出 Web 页面,这就是它的价值所以,**你完全不需要从 0 开始去实现这部分以后的内容**,在“又不是不能用”的前提下。 + + + +> 目前比如阿里卖家、美团外卖商家课堂等等项目使用了 Flutter Web + + + +再说 PC 端,PC 端本身的应用逻辑就和手机差异化很大:鼠标、键盘、可编窗口大小、横屏、滚动等这些方面,其实很难直接可以一套代码兼容,在我的理解更多是在 Android 和 iOS 上的一些控件、动画、UI、列表、业务逻辑等,可以在需要的时候直接在 PC 端上使用。**如果真的需要比较好的体验,个人建议还是至少把 PC 和 Mobile 分开两个业务项目实现**。 + +那如果真的要一套代码,有什么好的支持吗 ?也是有的,例如: `responsive_framework` 。 + + + +![image21](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image32) + +![image22](http://img.cdn.guoshuyu.cn/20220627_Flutter-FF/image33) \ No newline at end of file diff --git a/Flutter-GB.md b/Flutter-GB.md new file mode 100644 index 0000000..0f45bc7 --- /dev/null +++ b/Flutter-GB.md @@ -0,0 +1,279 @@ +# 完整解析使用 Github Action 构建和发布 Flutter 应用 + +Github Actions 是 Github 提供的免费自动化构建实现,特别适用于持续集成和持续交付的场景,它具备自动化完成许多不同任务的能力,例如构建、测试和部署等等。 + +## 一、简单介绍 + +用户只需要在自己 Github 的开源项目下创建 `.github/workflows` 脚本就可以完成接入,另外针对 Github Actions 官方还提供了 [marketplace](https://github.com/marketplace/actions) 用于开发者提交或者引用别人写好的 aciton ,**所以很多时候开发者在使用 Github Actions 时,其实会变成了在 [marketplace](https://github.com/marketplace/actions) 里挑选和组合 action 的场景。当然,这样各有利弊,后面我们会讲到** 。 + +![image-20220330110809824](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image1) + +要在 Github 存储库中使用 Github Actions,首先需要创建目录`.github/workflows/`,然后在 `workflows` 文件夹里创建不同的 `.yml` 文件用于响应或者执行不同的事件,比如 ` git push` 、`pull request ` 等,例如: + +```yaml +name: GitHub Actions Demo +on: [push] +jobs: + Explore-GitHub-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v2 + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ github.workspace }} + - run: echo "🍏 This job's status is ${{ job.status }}." +``` + +上面是 [Github doc](https://docs.github.com/en/actions/quickstart) 里关于 Action 的一个基本的工作流 yml 文件,具体参数含义 : + +- **name**:这表示该工作流文件的名称,将在 Github 的 actions 选项卡作为名称显示 ; +- **on**: 这将触发该工作流的事件名称,它可以包含事件列表,例如这里监听的事 `push`; +- **jobs**: 每个工作流会包含一个或多个 jobs ,在这里只有一个,主要是用于表示不同工作任务; +- **Explore-GitHub-Actions** :这是工作 ID,你也可以根据自己的需要命名,会在 action 的执行过程中显示; +- **runs-o**: jobs 需要运行在虚拟机上,在这里中使用了 `ubuntu-latest`,当然你也可以使用`windows-latest ` 或者 `macos-latest`; +- **steps**:每个 jobs 可以将需要执行的内容划分为不同步骤; +- **run**: 用于提供执行命令,例如这里使用了`echo` 打印日志; +- **name** :steps 里的 name 是可选项,主要是在日志中用来做标记的; +- **uses** : 使用一些官方或者第三方的 actions 来执行,例如这里使用官方的 `actions/checkout@v2`,它会check-out 我们的 repo ,之后工作流可以直接访问 repo 里的文件; + +在 GitHub 仓库添加完对应的 `.github/workflows/ci.yml` 文件之后,以后每次 `push` 都可以触发 action 的自动执行,以此来完成可持续的自动集成和构建能力。 + +![image-20220330112846187](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image2) + + + +## 二、构建 Flutter 和发布到 Github Release + +简单介绍完 Github Action ,接着我们介绍如何利用 Github Action 构建 Flutter 和发布 apk 到 Github Release,如下代码所示是 [gsy_github_app_flutter](https://github.com/CarGuo/gsy_github_app_flutter) 项目里使用到的 github action 脚本: + +```yaml +name: CI + +on: + push: + branches: + - master + tags: + - '*' + pull_request: + paths-ignore: + - '**/*.md' + - '**/*.txt' + - '**/*.png' + - '**/*.jpg' + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 11 + - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.8.1' + - uses: finnp/create-file-action@master + env: + FILE_NAME: lib/common/config/ignoreConfig.dart + FILE_DATA: class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";} + - run: flutter pub get + - run: flutter build apk --release --target-platform=android-arm64 --no-shrink + + apk: + name: Generate APK + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup JDK + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: 8 + - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.5.3' + - uses: finnp/create-file-action@master + env: + FILE_NAME: lib/common/config/ignoreConfig.dart + FILE_DATA: class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";} + - run: flutter pub get + - run: flutter build apk --release --target-platform=android-arm64 --no-shrink + - name: Upload APK + uses: actions/upload-artifact@v2 + with: + name: apk + path: build/app/outputs/apk/release/app-release.apk + release: + name: Release APK + needs: apk + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Download APK from build + uses: actions/download-artifact@v2 + with: + name: apk + - name: Display structure of downloaded files + run: ls -R + + - name: Create Release + id: create_release + uses: actions/create-release@v1.1.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + - name: Upload Release APK + id: upload_release_asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./app-release.apk + asset_name: app-release.apk + asset_content_type: application/zip +``` + +根据上述脚本,首先可以看到: + +- 在 `push` 事件里我们指定了只监听 master 分支和 tags 相关的提交; + +- 然后在 `pull_request` 事件里忽略了关于 .md、 .text 和图片相关的内容,也就是这部分内容提交不触发 action ,具体可以看你自己的需求; + +- 接着进入到 jobs 里,首先不管是 `push` 还是 `pull_request` 都会执行到 `Build` 事件,运行在 `ubuntu-latest` 虚拟机上,之后利用 `actions/checkout@v2` checkout 代码; + +- 接着使用 `actions/setup-java@v2` 配置 java 环境,这里使用的是 `Zulu OpenJDK` 版本 11 ,下面表格是 setup-java 支持的可选 java 类型; + + | Keyword | Distribution | Official site | License | + | -------------------------- | -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | + | `temurin` | Eclipse Temurin | [Link](https://adoptium.net/) | [Link](https://adoptium.net/about.html) | + | `zulu` | Zulu OpenJDK | [Link](https://www.azul.com/downloads/zulu-community/?package=jdk) | [Link](https://www.azul.com/products/zulu-and-zulu-enterprise/zulu-terms-of-use/) | + | `adopt` or `adopt-hotspot` | Adopt OpenJDK Hotspot | [Link](https://adoptopenjdk.net/) | [Link](https://adoptopenjdk.net/about.html) | + | `adopt-openj9` | Adopt OpenJDK OpenJ9 | [Link](https://adoptopenjdk.net/) | [Link](https://adoptopenjdk.net/about.html) | + | `liberica` | Liberica JDK | [Link](https://bell-sw.com/) | [Link](https://bell-sw.com/liberica_eula/) | + | `microsoft` | Microsoft Build of OpenJDK | [Link](https://www.microsoft.com/openjdk) | [Link](https://docs.microsoft.com/java/openjdk/faq) | + +- 接着就是使用第三方的 `subosito/flutter-action@v1` 配置 flutter 环境,直接通过 `flutter-version: '2.8.1'` 指定了 Flutter 版本; + +- 接着是使用第三方的 ` finnp/create-file-action@master` 创建文件,因为 [gsy_github_app_flutter](https://github.com/CarGuo/gsy_github_app_flutter) 项目有一个配置文件是需要用户根据自己的 ID 和 SECRET 手动创建,所以这里通过 create-file-action 创建文件并输入内容; + +- 在上述输入内容部分,有一个 `secrets.xxx` 的参数,因为构建时需要将自己的一些密钥信息配置到 action 里,所以如下图所示,可以在 `Settings` 的 `Secrets` 里添加对应的内容,就可以在 action 里通过 `secrets.xxx` 读取; + + ![image-20220330114509039](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image3) + +- 接着配置好环境之后,就可以执行 `flutter pub get` 和 ` flutter build apk` 执行构建; + +完成 Build 任务的逻辑介绍之后,可以看到在 Build 任务下面还有一个 apk 任务,该任务基本和 Build 任务一直,不同之处在于: + +- 多了一个 `if: startsWith(github.ref, 'refs/tags/')` ,也就是存在 tag 的时候才会触发该任务执行; +- 多了一个 `actions/upload-artifact@v2` 用于将构建出来的 `build/app/outputs/apk/release/app-release.apk`上传,并等到 release 任务内使用; + +完成 apk 任务之后,会进入到 release 任务,该任务同样通过 if 指定了只在 tag 提交时运行: + +- 任务首先会通过 `actions/download-artifact@v2` 下载刚刚上传的 apk; +- 然后就通过 `actions/create-release@v1.1.4` 创建一个 release 版本,这里使用的 `secrets.GITHUB_TOKEN ` 是官方内置的 secrets ,我们直接使用就可以了; +- 最后通过 `actions/upload-release-asset@v1.0.1` 将 apk 上传到刚刚创建的 release 版本里,自此就完成了 action 的发布流程; + +**可以看到整个过程其实都是在组合不同的 action ,可以很灵活方便地配置构建逻辑**,例如如果你的项目是单纯的 android sdk 项目,那同样可以通过如下脚本进行发布管理: + +```yaml +name: CI + +on: + push: + branches: + - master + paths-ignore: + - '.idea/**' + - '.gitattributes' + - '.github/**.json' + - '.gitignore' + - '.gitmodules' + - '**.md' + - '**/*.txt' + - '**/*.png' + - '**/*.jpg' + - 'LICENSE' + - 'NOTICE' + pull_request: + paths-ignore: + - '.idea/**' + - '.gitattributes' + - '.github/**.json' + - '.gitignore' + - '.gitmodules' + - '**.md' + - '**/*.txt' + - '**/*.png' + - '**/*.jpg' + - 'LICENSE' + - 'NOTICE' + +jobs: + publish: + name: Publish to MavenLocal + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + - uses: gradle/gradle-build-action@v2 + with: + arguments: publishToMavenLocal + + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + - uses: gradle/gradle-build-action@v2 + with: + arguments: app:assembleDebug +``` + +当然,如果你需要打包的是 iOS ,那么你就需要使用 `macos-latest` 的环境,另外还需要配置相关的开发者证书,这个过程可能会比较难受,相关可以参考 [《Flutter 搭建 iOS 命令行服务打包发布全保姆式流程》](https://juejin.cn/post/6953144821611495431) 。 + + + +## 三、隐私安全问题 + + + +最后,**关于 Github Actions 之前存在过出现泄露敏感数据的问题,比如 Github 的 Token 等** ,举个例子,如上面的脚本,它在执行任务时都会需要秘钥 ,如果你使用的第三方 action 在执行过程中获取了你的密钥并干了一些“非法” 的事情,就可能出现异常泄漏问题。 + +![image-20220330132722744](http://img.cdn.guoshuyu.cn/20220627_Flutter-GB/image4) + +**所以一般情况下建议大家都要去看下非官方的脚本实现里是否安全**,但是由于 tag 和 branch 是可以修改,所以建议不要@分支或tag,而是应该 checkout 对应的提交哈希,这样有利于你审查使用时的脚本是否安全。 + +另外,例如还有人提到可以通过 pull_request 来恶意攻击获取对应隐私: + +* 1、fork 一个正在使用 GitHub Actions 的公开代码库; + +* 2、创建一个基于该项目的 pull 请求; + +* 3、使用 pull_request_target 事件创建一个恶意 Actions 工作流,然后单独向该 fork 库 commit; + +* 4、将第二步基分支的 pull 请求更新为第三步的 commit 哈希; + +之后恶意 Actions 工作流就会运行,并从目标 repos 里获取到执行过程的敏感数据,此时攻击者将拥有对目标存储库的写访问权限,除此之外他们还可以通过 GitHub 访问与仓库之成的任何服务。 + +**所以虽然 GitHub Action 很便捷,但是如果出于商业考虑的话,还需要谨慎抉择安全问题**。 \ No newline at end of file diff --git a/Flutter-N1.md b/Flutter-N1.md new file mode 100644 index 0000000..442cc80 --- /dev/null +++ b/Flutter-N1.md @@ -0,0 +1,179 @@ +# Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty + +**今天分享一个简单轻松的内容: `ButtonStyle` 和 `MaterialStateProperty`** 。 + +大家是否还记得去年 Flutter 2.0 发布的时候,除了空安全之外 ,还有更新一系列关于控件的 breaking change,其中就有 `FlatButton` 被标志为弃用,需要替换成 `TextButton` 的情况。 + +如今已经 Flutter 3.0 ,不大知道大家对 `TextButton` 是否已经足够了解,或者说对 `MaterialStateProperty` 是否已经足够了解? + +为什么 `TextButton` 会和 `MaterialStateProperty` 扯到一起? + +首先,说到 `MaterialStateProperty ` 就不得不提 Material Design ,**`MaterialStateProperty` 的设计理念,就是基于 Material Design 去针对全平台的交互进行兼容**。 + +![image-20220530103804444](http://img.cdn.guoshuyu.cn/20220531_N/image1.png) + +相信大家当初在从 Flutter 1 切换到 Flutter 2 的时候,应该都有过这样一个疑问: + +> **为什么 `FlatButton` 和 `RaisedButton` 会被弃用替换成 `TextButton ` 和 `RaisedButton`** ? + +![image-20220530104346216](http://img.cdn.guoshuyu.cn/20220531_N/image2.png) + +因为以前只需要使用 `textColor` 、`backgroundColor` 等参数就可以快速设置颜色,但是现在使用 `ButtonStyle` ,从代码量上看相对会麻烦不少。 + +当然,**在后续里官方也提供了类似 `styleFrom` 等静态方法来简化代码,但是本质上切换到 `ButtonStyle` 的意义是什么 ?`MaterialStateProperty` 又是什么**? + +![image-20220530104739603](http://img.cdn.guoshuyu.cn/20220531_N/image3.png) + +首先我们看看 `MaterialStateProperty` ,在 `MaterialStateProperty` 体系里有一个 `MaterialState` 枚举,它主要包含了: + +- disabled:当控件或元素不能交互性时 +- hovered:鼠标交互悬停时 +- focused: 在键盘交互中突出显示 +- selected:例如 check box 的选定状态 +- pressed:通过鼠标、键盘或者触摸等方法发起的轻击或点击 +- dragged:用户长按并移动控件时 +- error:错误状态下,比如 `TextField` 的 Error + +![image-20220530114532550](http://img.cdn.guoshuyu.cn/20220531_N/image4.png) + +所以现在理解了吧? 随着 Web 和 Desktop 平台的发布,原本的 `FlatButton` 无法很好满足新的 UI 交互需要,例如键鼠交互下的 hovered ,**所以 `TextButton ` 开始使用 `MaterialStateProperty` 来组成 `ButtonStyle` 支持不同平台下 UI 的状态展示**。 + +在此之前,如果需要多平台适配你可能会这么写,你需要处理很多不同的状态条件,从而产生无数` if` 或者 `case` : + +```dart + getStateColor(Set states) { + if (states.contains(MaterialState.hovered)) { + ///在 hovered 时还 focused 了 + if (states.contains(MaterialState.focused)) { + return Colors.red; + } else { + return Colors.blue; + } + } else if (states.contains(MaterialState.focused)) { + return Colors.yellow; + } + return Colors.green; + } +``` + +但是现在, 你只需要继承 `MaterialStateProperty` 然后 @override `resolve` 方法就可以了,例如 `TextButton ` 里的 hovered 效果,在 `TextButton ` 内默认就是通过 `_TextButtonDefaultOverlay` 实现,对 `primary.withOpacity` 来实现 hovered 效果。 + +```dart +@immutable +class _TextButtonDefaultOverlay extends MaterialStateProperty { + _TextButtonDefaultOverlay(this.primary); + + final Color primary; + + @override + Color? resolve(Set states) { + if (states.contains(MaterialState.hovered)) + return primary.withOpacity(0.04); + if (states.contains(MaterialState.focused) || states.contains(MaterialState.pressed)) + return primary.withOpacity(0.12); + return null; + } + + @override + String toString() { + return '{hovered: ${primary.withOpacity(0.04)}, focused,pressed: ${primary.withOpacity(0.12)}, otherwise: null}'; + } +} +``` + +![](http://img.cdn.guoshuyu.cn/20220531_N/image5.gif) + + + +其实在 `TextButton ` 的内部,默认同样是通过 `styleFrom` 来配置所需的 `MaterialState` 效果,其中有: + +- `_TextButtonDefaultForeground` : 用于处理 disabled ,通过 `onSurface?.withOpacity(0.38)` 变化颜色; +- `_TextButtonDefaultOverlay`: 用于处理 hovered 、 focused 和 pressed ,通过 `primary.withOpacity` 变化颜色; +- `_TextButtonDefaultMouseCursor` : 用于处理鼠标 MouseCursor 的 disabled; + +剩下的参则是通过我们熟悉的 ` ButtonStyleButton.allOrNull` 进行添加,也就是不需要特殊处理的参数。 + +那 ` ButtonStyleButton.allOrNull` 的作用是什么? + +其实 ` ButtonStyleButton.allOrNull` 就是 `MaterialStateProperty.all` 方法的可 null 版本,对应内部实现最终还是实现了 `resolve` 接口的 `MaterialStateProperty` ,所以如果需要支持 null,你也可以做直接使用 `MaterialStateProperty.all` 。 + +```dart +static MaterialStateProperty? allOrNull(T? value) => value == null ? null : MaterialStateProperty.all(value); +``` + +![image-20220530142530429](http://img.cdn.guoshuyu.cn/20220531_N/image6.png) + +当然,如果不想创建新的 class 但是又想定制逻辑,如下代码所示,那你也可以使用 `resolveWith` 静态方法: + +````dart +TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.hovered)) { + return Colors.green; + } + return Colors.transparent; + })), + onPressed: () {}, + child: new Text( + "TEST", + style: TextStyle(fontSize: 100), + ), +), +```` + +![](http://img.cdn.guoshuyu.cn/20220531_N/image7.gif) + + + +当然,谷歌在对 Flutter 控件进行 `MaterialState` 的 UI 响应时,也是遵循了 Material Design 的设计规范,比如 Hover 时 `primary.withOpacity(0.04);` ,所以不管在 `TextButton` 还是 `RaisedButton` 内部都遵循类似的规范。 + + + +![image-20220530113735250](http://img.cdn.guoshuyu.cn/20220531_N/image8.png) + + + +另外,有时候你肯定不希望每个地方单独去配置 Style ,那这时候你就需要配合 Theme 来实现。 + +事实上 `TextButton` 、 `ElevatedButton ` 和 `OutlinedButton` 都是 `ButtonStyleButton` 的子类,他们都会遵循以下的原则: + +```dart + final ButtonStyle? widgetStyle = widget.style; + final ButtonStyle? themeStyle = widget.themeStyleOf(context); + final ButtonStyle defaultStyle = widget.defaultStyleOf(context); + assert(defaultStyle != null); + + T? effectiveValue(T? Function(ButtonStyle? style) getProperty) { + final T? widgetValue = getProperty(widgetStyle); + final T? themeValue = getProperty(themeStyle); + final T? defaultValue = getProperty(defaultStyle); + return widgetValue ?? themeValue ?? defaultValue; + } +``` + +也就是 ` return widgetValue ?? themeValue ?? defaultValue;` ,其中: + +- widgetValue 就是控件单独配置的样式 +- themeValue 就是 Theme 里配置的全局样式 +- defaultValue 就是默认内置的样式,也即是 `styleFrom` 静态方法,当然 `styleFrom` 里也会用一些 `ThemeData` 的对象,例如 `colorScheme.primary` 、 `textTheme.button` 、`theme.shadowColor` 等 + +所以,例如当你需要全局去除按键的水波纹时,如下代码所示,你可以修改 `ThemeData` 的 `TextButtonTheme` 来实现,因为 `TextButton` 内的 `themeStyleOf` 使用的就是 `TextButtonTheme` 。 + +```dart +theme: ThemeData( + primarySwatch: Colors.blue, + textButtonTheme: TextButtonThemeData( + // 去掉 TextButton 的水波纹效果 + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ), +), +``` + +![image-20220530151634041](http://img.cdn.guoshuyu.cn/20220531_N/image9.png) + +最后做个总结: + +- 如果只是简单配置背景颜色,可以直接用 `styleFrom` +- 如果单独配置,可以使用 ` ButtonStyleButton.allOrNull` +- 如果需要灵活处理,可以使用 ` ButtonStyleButton.resolveWith` 或者实现 `MaterialStateProperty` 的 `resolve` 接口 \ No newline at end of file diff --git a/Flutter-N2.md b/Flutter-N2.md new file mode 100644 index 0000000..d7e73bd --- /dev/null +++ b/Flutter-N2.md @@ -0,0 +1,213 @@ +# Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3 + +**本篇分享一个简单轻松的内容: `ThemeExtensions ` 和 `Material3`** ,它们都是 Flutter 3.0 中的重要组成部分,相信后面的小知识你可能还没了解过~。 + +# ThemeExtensions + +相信大家都用过 Flutter 里的 ` Theme` ,在 Flutter 里可以通过修改全局的 ` ThemeData` 就来实现一些样式上的调整,比如 :全局去除 `InkWell` 和 `TextButton` 的点击效果。 + +```dart +theme: ThemeData( + primarySwatch: Colors.blue, + // 去掉 InkWell 的点击水波纹效果 + splashFactory: NoSplash.splashFactory, + // 去除 InkWell 点击的 highlight + highlightColor: Colors.transparent, + textButtonTheme: TextButtonThemeData( + // 去掉 TextButton 的水波纹效果 + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ), +), +``` + +当然,开发者也可以通过 `Theme.of(context)` 去读取 `ThemeData` 的一些全局样式,从而让自己的控件配置更加灵活,**但是如果 `ThemeData` 里没有符合你需求的参数,或者你希望这个参数只被特定控件是用,那该怎么办** ? + +Flutter 3 给我们提供了一个解决方案: `ThemeExtensions ` 。 + +开发者可以通过继承 `ThemeExtension` 并 override 对应的 `copyWith` 和 `lerp` 方法来自定义需要拓展的 `ThemeData` 参数,比如这样: + +```dart +@immutable +class StatusColors extends ThemeExtension { + static const light = StatusColors(open: Colors.green, closed: Colors.red); + static const dark = StatusColors(open: Colors.white, closed: Colors.brown); + + const StatusColors({required this.open, required this.closed}); + + final Color? open; + final Color? closed; + + @override + StatusColors copyWith({ + Color? success, + Color? info, + }) { + return StatusColors( + open: success ?? this.open, + closed: info ?? this.closed, + ); + } + + @override + StatusColors lerp(ThemeExtension? other, double t) { + if (other is! StatusColors) { + return this; + } + return StatusColors( + open: Color.lerp(open, other.open, t), + closed: Color.lerp(closed, other.closed, t), + ); + } + + @override + String toString() => 'StatusColors(' + 'open: $open, closed: $closed' + ')'; +} +``` + +之后就可以将上面的 `StatusColors` 配置到 `Theme` 的 `extensions` 上,然后通过 ` Theme.of(context).extension()` 读取配置的参数。 + +```dart +theme: ThemeData( + primarySwatch: Colors.blue, + extensions: >[ + StatusColors.light, + ], +), + +····· + +@override +Widget build(BuildContext context) { + + /// get status color from ThemeExtensions + final statusColors = Theme.of(context).extension(); + + return Scaffold( + extendBody: true, + body: Container( + alignment: Alignment.center, + child: new ElevatedButton( + style: TextButton.styleFrom( + backgroundColor: statusColors?.open, + ), + onPressed: () {}, + child: new Text("Button")), + ), + ); +} +``` + +是不是很简单?**通过 `ThemeExtensions ` ,第三方 package 在编写控件时,也可以提供对应的 `ThemeExtensions` 对象,实现更灵活的样式配置支持**。 + +# Material3 + +Material3 又叫 MaterialYou , 是谷歌在 Android 12 时提出的全新 UI 设计规范,现在 Flutter 3.0 里你可以通过 `useMaterial3: true` 打开配置支持。 + +```dart +theme: ThemeData( + primarySwatch: Colors.blue, + ///打开 useMaterial3 样式 + useMaterial3: true, +), +``` + +当然,**在你开启 Material3 之前,你需要对它有一定了解,因为它对 UI 风格的影响还是很大的,知己知彼才能不被背后捅刀**。 + +如下图所示,是在 `primarySwatch: Colors.blue` 的情况下,`AppBar` 、`Card`、`TextButton`、 `ElevatedButton` 的样式区别: + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image1.png) + +可以看到圆角和默认的颜色都发生了变化,并且除了 UI 更加圆润之外,交互效果也发生了一些改变,比如: + +- 点击效果和 `Dialog` 的默认样式都发生了变化; +- Android 上列表滚动的默认 `OverscrollIndicator` 效果也发生了改变; + +| 交互 | 列表 | +| ------------------------------------------------------ | ------------------------------------------------------------ | +| ![](http://img.cdn.guoshuyu.cn/20220605_N2/image2.gif) | ![333333](http://img.cdn.guoshuyu.cn/20220605_N2/image3.gif) | + +目前在 Flutter 3 中受到 `useMaterial3` 影响的主要有以下这些 Widget ,可以看到主要影响的还是具有交互效果的 Widget 居多: + +* [AlertDialog] + +* [AppBar] +* [Card] +* [Dialog] +* [ElevatedButton] +* [FloatingActionButton] +* [Material] +* [NavigationBar] +* [NavigationRail] +* [OutlinedButton] +* [StretchingOverscrollIndicator] +* [GlowingOverscrollIndicator] +* [TextButton] + +**那 Material3 和之前的 Material2 有什么区别呢**? + +以 `AppBar` 举例,可以看到在 M2 和 M3 中背景颜色的获取方式就有所不同,在 M3 下没有了 `Brightness.dark` 的判断,那是说明 M3 不支持暗黑模式吗? + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image4.png) + +回答这个问题之前,我们先看 `_TokeDefaultsM3` 有什么特别之处,从源码注释里可以看到 `_TokeDefaultsM3` 是通过脚本自动生成,并且目前版本号是 `v0_92` ,**所以 M3 和 M2 最大的不同之一就是它的样式代码现在是自动生成**。 + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image5.png) + +在 Flutter 的 [gen_defaults](https://github.com/flutter/flutter/tree/ca2d60e8e2344d8c0ed938869f7c974cb745e841/dev/tools/gen_defaults/lib) 下就可以看到,基本上涉及 M3 的默认样式,都是通过 `data` 下的数据利用模版自动生成,比如 `Appbar` 的 `backgroundColor` 指向的就是 `surface` 。 + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image6.png) + +**而之所以 M3 的默认样式不再需要 `Brightness.dark` 的判断,是因为在 M3 使用的 `ColorScheme` 里已经做了判断**。 + +![image-20220602214139954](http://img.cdn.guoshuyu.cn/20220605_N2/image7.png) + +**事实上现在 Flutter 3.0 里 `colorScheme` 才是主题颜色的核心,而 `primaryColorBrightness` 和 `primarySwatch` 等参数在未来将会被弃用**,所以如果目前你还在使用 `primarySwatch` ,在 `ThemeData` 内部会通过 `ColorScheme.fromSwatch` 方法转换为 `ColorScheme` 。 + +```dart +ColorScheme.fromSwatch( + primarySwatch: primarySwatch, + primaryColorDark: primaryColorDark, + accentColor: accentColor, + cardColor: cardColor, + backgroundColor: backgroundColor, + errorColor: errorColor, + brightness: effectiveBrightness, +); +``` + +另外你也可以通过 `ColorScheme.fromSeed` 或者 `colorSchemeSeed ` 来直接配置 `ThemeData` 里的 `ColorScheme` ,**那 `ColorScheme` 又是什么** ? + +```dart +theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF4285F4)), + ///打开 useMaterial3 样式 + useMaterial3: true, +), +``` + +**这里其实就涉及到一个很有趣的知识点:Material3 下的 HCT 颜色包: [material-color-utilities](https://github.com/material-foundation/material-color-utilities)** 。 + +在 Material3 下颜色其实不是完全按照 RGB 去计算,而是会经过 [material-color-utilities](https://github.com/material-foundation/material-color-utilities) 的转化,通过内部的 `CorePalette` 对象,RGB 会转化为 HCT 相关的值去计算显示。 + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image8.png) + +对于 HCT 其实是 Hue、Chroma、Tone 三个单词的缩写,可以解释为色相、色度和色调,通过谷歌开源的 [material-color-utilities](https://github.com/material-foundation/material-color-utilities) 插件就可以方便实现 HCT 颜色空间的接入,目前该 repo 已支持 Dart、Java 和 Typecript 等语言,另外 C/C++ 和 Object-C 也在即将支持。 + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image9.png) + +得益于 HCT ,例如我们前面的 `ColorScheme.fromSeed(seedColor: Color(0xFF4285F4))`,就可以通过一个 seedColor 直接生成一系列主题颜色,这就是 Material3 里可以拥有更丰富的主题色彩的原因。 + +![](http://img.cdn.guoshuyu.cn/20220605_N2/image10.png) + +> 更多可见 [《HCT 的色彩原理》](https://material.io/blog/science-of-color-design) + +# 最后 + +最后我们回顾一下,今天的小技巧有: + +- 通过 `ThemeExtensions` 拓展想要的自定义 `ThemeData` +- 通过 `useMaterial3` 启用 Material3 ,并通过 `ColorScheme` 配置更丰富的 HCT 颜色 + +好了,现在你可以去问你的设计师:你知道什么是 HCT 么? \ No newline at end of file diff --git a/Flutter-N3.md b/Flutter-N3.md new file mode 100644 index 0000000..1025d02 --- /dev/null +++ b/Flutter-N3.md @@ -0,0 +1,197 @@ +# Flutter 小技巧之玩转字体渲染和问题修复 + +这次的 Flutter 小技巧是字体渲染,虽然是小技巧但是内容略长,可能大家在日常开发中不会特别关心字体相关的部分,**而这将是一篇你平时可能用不到 ,但是遇到问题就会翻出来的文章**。 + +> 本篇将快速普及一些字体渲染相关的基础,解决一些因为字体而导致的异常问题,**并穿插一些实用小技巧**,内容篇幅可能略长,建议先 Mark 后看。 + +# 一、字体库 + +首先,问一个我经常问的面试题:**Flutter 在 Android 和 iOS 上使用了哪些字体**? + +如果你恰好看过 `typography.dart` 的源码和解释,你可以会有初步结论: + +- Android 上使用的是 `Roboto` 字体; +- iOS 上使用的是 `.SF UI Display` 或者 `.SF UI Text` 字体; + +![image-20220601135913731](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image1.png) + + 但是,如果你再进一步去了解就会发现,在加上中文显示之后,结论应该是: + +- 默认在 iOS 上: + - 中文字体:`PingFang SC` (繁体还有 `PingFang TC` 、 `PingFang HK` ) + - 英文字体:`.SF UI Text` / `.SF UI Display` +- 默认在 Android 上: + - 中文字体:`Source Han Sans` / `Noto` + - 英文字体:`Roboto` + +那这时候你可能会问:**.SF 没有中文,那可以使用 `PingFang` 显示英文吗**? 答案是可以的,但是字形和字重会有微妙区别, 例如下图里的 G 就有很明显的不同。 + +![image-20220601141145552](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image2.png) + +那如果加上韩文呢?这时候 iOS 上的 `PingFang` 和 `.SF` 就不够用了,需要调用如 `Apple SD Gothic Neo` 这样的超集字体库,而说到这里就需要介绍一个 Flutter 上你可能会遇到的 Bug。 + +如下图所示,**当在使用 `Apple SD Gothic Neo` 字体出现中文和韩文同时显示时,你可能会察觉一些字形很奇怪**,比如【推广】这两个字,其中【广】这个字符在超集上是不存在的,所以会变成了中文的【广】,但是【推】字用的还是超集里的字形。 + +![image-20220601141720525](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image3.png) + +这种情况下,最终渲染的结果会如下图所示,解决的思路也很简单,**小技巧就是给 `TextStyle` 或者 `Theme` 的 `fontFamilyFallback` 配置上 `["PingFang SC" , "Heiti SC"]`** 。 + +![image-20220601142805434](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image4.png) + +另外,如果你还对英文下 `.SF UI Display` 和 ``SF UI Text` 之间的关系困惑的话,那其实你不用太过纠结,因为从 SF 设计上大概意思上理解的话: + +> .SF Text 适用于更小的字体;.SF Display 则适用于偏大的字体,分水岭大概是 20pt 左右,不过 SF(San Francisco) 属于动态字体,系统会动态匹配。 + + + +# 二、Flutter Text + +虽然上面介绍字体的一些相关内容,但是在 Flutter 上和原生还是有一些差异,在 Flutter 中的文本呈现逻辑是有分层的,其中: + +- 衍生自 Minikin 的 libtxt 库用于字体选择,分隔行等; +- HartBuzz 用于字形选择和成型; +- Skia作为 渲染 / GPU后端; +- **在 Android / Fuchsia 上使用 FreeType 渲染,在 iOS 上使用CoreGraphics 来渲染字体** 。 + +## Text Height + +那如果这时候我问你一个问题: **一个 ` fontSize: 100` 的 H 字母需要占据多大的高度** ?你会回答多少? + +首先,我们用一个 100 的红色 `Container` 和 ` fontSize: 100` 的 H 文本做个对比,可以看到 H 文本所在的蓝色区域其实是需要大于 100 的红色区域的。 + +![image-20220601145346189](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image5.png) + +**事实上,前面的蓝色区域是字体的行高,也就是 line height**,关于这个行高,首先需要解释的就是 `TextStyle` 中的 `height` 参数。 + +默认情况下 `height` 参数是 `null`,当我们把它设置为 **`1`** 之后,如下图所示,可以看到蓝色区域的高度和红色小方块对齐,变成了 **100** 的高度,也就是行高变成了 **100** ,而 **H** 字母完整地显示在了蓝色区域内。 + +![image-20220601145634196](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image6.png) + +那 `height` 是什么呢?首先 `TextStyle` 中的 `height` 参数值在设置后,其效果值是 `fontSize` 的倍数: + +- 当 `height` 为空时,行高默认是使用字体的**量度**(这个**量度**后面会有解释); +- 当 `height` 不是空时,行高为 `height` * `fontSize` 的大小; + +如下图所示,蓝色区域和红色区域的对比就是 `height` 为 `null` 和 `1` 的对比高度。 + +![image-20220601145710275](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image7.png) + +所以,看到这里你又知道了一个小技巧:**当文字在 `Container` “有限高度” 内容内无法居中时,可以考虑调整 `TextStyle` 中的 `height` 来实现** 。 + +![image-20220601151621858](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image8.png) + +> 当然,这时候如果你把 `Container` 的 `height:50` 去掉,又会是另外一个效果。 + +所以 height 参数和文本渲染的高度之间是成倍数关系,具体如下图所示,同时最需要注意的点就是:**文本内容在 height 里并不是居中,这里的 height 可以类比于调整行高。** + +![image-20220601151923432](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image9.png) + +另外,**文本中的除了 `TextStyle` 下的 `height` 之外,还是有 `StrutStyle` 参数下的 `height`** ,它影响的是字体的整体量度,也就是如下图所示,影响的是 ascent - descent 的高度。 + +![image-20220601152843273](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image10.png) + +**那你说它和 `TextStyle` 下的 `height` 有什么区别**? 如下图所示例子: + +- `StrutStyle` 的 `froceStrutHeight` 开启后,`TextStyle` 的 `height` 不会生效; +- `StrutStyle` 设置 `fontSize:50` 影响的内容和 `TextStyle` 的 `fontSize:100` 影响的内容不一样; + +![](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image11.png) + +另外在 `StrutStyle` 里还有一个叫 `leading` 的 参数,加上了 `leading` 后才是 Flutter 中对字体行高完全的控制组合,`leading` 默认为 `null` ,同时它的效果也是 `fontSize` 的倍数,并且分布是上下均分。 + +![](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image12.png)所以,看到这里你又知道了一个小技巧:**设置 `leading` 可以均分高度,所以如下图所示,也可以用于调整行间距。** + +![image-20220601154712338](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image13.png) + +> 更多行高相关可见 :[《深入理解 Flutter 中的字体“冷”知识》](https://juejin.cn/post/6844904174023344136) + +## FontWeight + +另外一个关于字体的知识点就是 `FontWeight` ,相信大家对 `FontWeight` 不会陌生,比如我们默认的 normal 是 w400,而常用的 bold 是 w700 ,整个 `FontWeight` 列表覆盖 100-900 的数值。 + +![image-20220601155236983](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image14.png) + +那么这里又有个问题:**这些 Weight 在字体里都能找到对应的粗细吗**? + +答案是不行的,因为正常情况下如下图所示 ,有些字体库在某些 Weight 下是没有对应支持,例如 + +- Roboto 没有 w600 +- PingFang 没有高于 w600 + +![image-20220601162130629](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image15.png) + + + +**那你可能好奇,为什么这里要特意介绍 FontWeight ?因为在 Flutter 3.0 目前它对中文有 Bug**! + +从下面这张图你可以看到,在 Flutter 3.0 上中文从 100-500 的字重显示是不正常的,肉眼可以看出在 100 - 500 都显示同一个字重。 + +![image-20220601162935325](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image16.png) + +> 这个 Bug 来自于当 `SkParagraph` 调用 `onMatchFamilyStyleCharacter` 时,`onMatchFamilyStyleCharacter` 的实现没有选择最接近 `TextStyle` 的字体,所以在 `CTFontCreateWithFontDescriptor` 时会带上 weight 参数但是却没有 `familyName` ,所以 CTFontCreateWithFontDescriptor` 函数就会返回 Helvetica 字体的默认 weight。 + +临时解决小技巧也很简单:**全局设置 `fontFamilyFallback: ["PingFang SC"]` 或者 `fontFamily: 'PingFang SC'` 就可以解决,又是 Fallback , 这时候你就会发现,前面介绍的字体常识,可以在这里快速被利用起来**。 + +![image-20220601163255325](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image17.png) + +> 因为 iOS 上中文就是 `PingFang SC` ,只要 Fallback 回 PingFang 就可以正常渲染,而这个问题在 Android 模拟器、iOS 真机、Mac 上等会出现,但是 Android 真机上却不会,该问题我也提交在 [#105014](https://github.com/flutter/flutter/issues/105014) 下开始跟进。 + +添加的 Fallback 之后效果如上图左侧所示, 那 Fallback 的作用是什么? + +前面我们介绍过,系统在多语言中渲染是需要多种字体库来支持,而当找不到字形时,就要依赖提供的 Fallback 里的有序列表,例如: + +> 如果在 [fontFamily](https://api.flutter.dev/flutter/painting/TextStyle/fontFamily.html) 中找不到字形,则在 [fontFamilyFallback](https://api.flutter.dev/flutter/painting/TextStyle/fontFamilyFallback.html) 中搜索,如果没有找到,则会在返回默认字体。 + +另外关于 `FontWeight` 还有一个“小彩蛋”,在 iOS 上,当用户在辅助设置里开启 Bold Text 之后,如果你使用的是 `Text` 控件,那么默认情况下所有的字体都会变成 w700 的粗体。 + +![image-20220601164236038](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image18.png) + +因为在 `Text` 内使用了 `MediaQuery.boldTextOverride` 判断,Flutter 会接收到 iOS 上用户开启了 Bold Text ,从而强行将 `fontWeight` 设置为 `FontWeight.bold ` ,当然如果你直接使用 `RichText` 就 没有这一行为。 + +![](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image19.png) + +这时候小技巧就又来了:**如果你不希望这些系统行为干扰到你,那么你可以通过嵌套 `MediaQuery` 来全局关闭,而类似的行为还有 `textScaleFactor` 和 `platformBrightness`等** 。 + +```dart +return MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false), + child: MaterialApp( + useInheritedMediaQuery: true, + ), +); +``` + +![image-20220531082324707](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image20.png) + + + +## FontFeature + +最后再介绍一个冷门参数 FontFeature 。 + +什么是 `FontFeature`? **简单来说就是影响字体形状的一个属性** ,在前端的对应领域里应该是 `font-feature-settings`,它有别于 `FontFamily` ,是用于指定字体内字的形状参数。 + +> 如下图所示是 `frac` 分数和 `tnum` 表格数字的对比渲染效果,这种效果可以在不增加字体库时实现特殊的渲染,另外 `Feature` 也有特征的意思,所以也可以理解为字体特征。 + +![image-20220601165224593](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image21.png) + +那 FontFeature 有什么用呢? 这里又有一个使用小技巧了:**当出现数字和文本同时出现,导致排列不对齐时,可以通过给 `Text` 设置 `fontFeatures: [FontFeature("tnum")]` 来对齐**。 + +例如下图左边是没有设置 fontFeatures 的情况,右边是设置了 `FontFeature("tnum")` 的情况,对比之下还是很明显的。 + +![image-20220601165855711](http://img.cdn.guoshuyu.cn/20220611_FONT¥/image22.png) + +> 更多关于 FontFeature 的内容可见 [《Flutter 上字体的另类玩法:FontFeature 》](https://juejin.cn/post/7078680758826565662) + +# 三、最后 + +总结一下,本篇内容信息量相对比较密集,主要涉及: + +- 字体基础 +- Text Height +- FontWeight +- FontFeature + +从以上四个方面介绍了 Flutter 开发里关于字体渲染的“冷知识”和小技巧,包括:解决多语言下的字体错误、如何正确调整行高、如何对其数字内容等相关小技巧。 + +如果你还有什么关于字体的疑问,欢迎留言讨论~ \ No newline at end of file diff --git a/Flutter-N4.md b/Flutter-N4.md new file mode 100644 index 0000000..2b85b30 --- /dev/null +++ b/Flutter-N4.md @@ -0,0 +1,242 @@ +# Flutter 小技巧之有趣的动画技巧 + +**本篇分享一个简单轻松的内容: 剖析 Flutter 里的动画技巧** ,首先我们看下图效果,如果要实现下面的动画切换效果,你会想到如何实现? + + + +![](http://img.cdn.guoshuyu.cn/20220619_N4/image1.gif) + + + +# 动画效果 + +事实上 Flutter 里实现类似的动画效果很简单,甚至不需要自定义布局,只需要通过官方的内置控件就可以轻松实现。 + +首先我们需要使用 `AnimatedPositioned` 和 `AnimatedContainer` : + +- `AnimatedPositioned` 用于在 `Stack` 里实现位移动画效果 +- `AnimatedContainer` 用于实现大小变化的动画效果 + +接着我们定义一个 `PositionItem` ,将 `AnimatedPositioned` 和 `AnimatedContainer` 嵌套在一起,并且通过 `PositionedItemData` 用于改变它们的位置和大小。 + +```dart +class PositionItem extends StatelessWidget { + final PositionedItemData data; + final Widget child; + + const PositionItem(this.data, {required this.child}); + + @override + Widget build(BuildContext context) { + return new AnimatedPositioned( + duration: Duration(seconds: 1), + curve: Curves.fastOutSlowIn, + child: new AnimatedContainer( + duration: Duration(seconds: 1), + curve: Curves.fastOutSlowIn, + width: data.width, + height: data.height, + child: child, + ), + left: data.left, + top: data.top, + ); + } +} +class PositionedItemData { + final double left; + final double top; + final double width; + final double height; + + PositionedItemData({ + required this.left, + required this.top, + required this.width, + required this.height, + }); +} +``` + +之后我们只需要把 `PositionItem` 放到通过 `Stack` 下,然后通过 `LayoutBuilder` 获得 `parent` 的大小,根据 `PositionedItemData` 调整 `PositionItem` 的位置和大小,就可以轻松实现开始的动画效果。 + +```dart +child: LayoutBuilder( + builder: (_, con) { + var f = getIndexPosition(currentIndex % 3, con.biggest); + var s = getIndexPosition((currentIndex + 1) % 3, con.biggest); + var t = getIndexPosition((currentIndex + 2) % 3, con.biggest); + return Stack( + fit: StackFit.expand, + children: [ + PositionItem(f, + child: InkWell( + onTap: () { + print("red"); + }, + child: Container(color: Colors.redAccent), + )), + PositionItem(s, + child: InkWell( + onTap: () { + print("green"); + }, + child: Container(color: Colors.greenAccent), + )), + PositionItem(t, + child: InkWell( + onTap: () { + print("yello"); + }, + child: Container(color: Colors.yellowAccent), + )), + ], + ); + }, +), +``` + +如下图所示,只需要每次切换对应的 index ,便可以调整对应 Item 的大小和位置发生变化,从而触发 `AnimatedPositioned` 和 `AnimatedContainer` 产生动画效果,达到类似开始时动图的动画效果。 + +| 计算大小 | 效果 | +| ------------------------------------------------------------ | ---------------------------------------------------------- | +| ![image-20220611180815516](http://img.cdn.guoshuyu.cn/20220619_N4/image2.png) | ![6666](http://img.cdn.guoshuyu.cn/20220619_N4/image3.gif) | + +> 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/anim_switch_layout_demo_page.dart + +如果你对于实现原理没兴趣,那到这里就可以结束了,通过上面你已经知道了一个小技巧: + +> **改变 `AnimatedPositioned` 和 `AnimatedContainer` 的任意参数,就可以让它们产生动画效果**,而它们的参数和 `Positioned` 与 `Container` 一模一样,所以使用起来可以无缝替换 `Positioned` 与 `Container` ,只需要简单配置额外的 `duration` 等参数。 + +# 进阶学习 + +**那 `AnimatedPositioned` 和 `AnimatedContainer` 是如何实现动画效果 ?这里就要介绍一个抽象父类 `ImplicitlyAnimatedWidget`** 。 + +> 几乎所有 Animated 开头的控件都是继承于它,既然是用于动画 ,那么 `ImplicitlyAnimatedWidget` 就肯定是一个 `StatefulWidget` ,那么不出意外,它的实现逻辑主要在于 `ImplicitlyAnimatedWidgetState` ,而我们后续也会通过它来展开。 + +首先我们回顾一下,一般在 Flutter 使用动画需要什么: + +- `AnimationController` : 用于控制动画启动、暂停 +- `TickerProvider` : 用于创建 `AnimationController` 所需的 `vsync` 参数,一般最常使用 `SingleTickerProviderStateMixin` +- `Animation` : 用于处理动画的 value ,例如常见的 `CurvedAnimation` +- 接收动画的对象:例如 `FadeTransition` + +简单来说,Flutter 里的动画是从 `Ticker` 开始,当我们在 `State` 里 `with TickerProviderStateMixin` 之后,就代表了具备执行动画的能力: + +> 每次 Flutter 在绘制帧的时候,`Ticker` 就会同步到执行 ` AnimationController` 里的 `_tick` 方法,然后执行 `notifyListeners` ,改变 `Animation` 的 value,从而触发 State 的 `setState` 或者 RenderObject 的 `markNeedsPaint` 更新界面。 + +举个例子,如下代码所示,可以看到实现一个简单动画效果所需的代码并不少,而且**这部分代码重复度很高,所以针对这部分逻辑,官方提供了 `ImplicitlyAnimatedWidget` 模版**。 + +```dart +class _AnimatedOpacityState extends State + with TickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + late final Animation _animation = CurvedAnimation( + parent: _controller, + curve: Curves.easeIn, + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + child: FadeTransition( + opacity: _animation, + child: const Padding(padding: EdgeInsets.all(8), child: FlutterLogo()), + ), + ); + } +} +``` + +例如上面的 Fade 动画,换成 `ImplicitlyAnimatedWidgetState` 只需要实现 `forEachTween` 方法和 `didUpdateTweens` 方法即可,而不再需要关心 `AnimationController` 和 `CurvedAnimation` 等相关内容。 + +```dart +class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState { + Tween? _opacity; + late Animation _opacityAnimation; + + @override + void forEachTween(TweenVisitor visitor) { + _opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween(begin: value as double)) as Tween?; + } + + @override + void didUpdateTweens() { + _opacityAnimation = animation.drive(_opacity!); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _opacityAnimation, + alwaysIncludeSemantics: widget.alwaysIncludeSemantics, + child: widget.child, + ); + } +} +``` + +**那 `ImplicitlyAnimatedWidgetState` 是如何做到改变 `opacity` 就触发动画?** + +关键还是在于实现的 `forEachTween` :当 `opacity` 被更新时,`forEachTween` 会被调用,这时候内部会通过 `_shouldAnimateTween` 判断值是否更改,如果目标值已更改,就执行基类里的 `AnimationController.forward` 开始动画。 + +![image-20220611170418125](http://img.cdn.guoshuyu.cn/20220619_N4/image4.png) + +> 这里补充一个内容:`FadeTransition` 内部会对 `_opacityAnimation` 添加兼容,当 `AnimationController` 开始执行动画的时候,就会触发 `_opacityAnimation` 的监听,从而执行 `markNeedsPaint` ,**而如下图所示, `markNeedsPaint` 最终会触发 RenderObject 的重绘**。 + +![image-20220611173533772](http://img.cdn.guoshuyu.cn/20220619_N4/image5.png) + +所以到这里,我们知道了:**通过继承 `ImplicitlyAnimatedWidget` 和 `ImplicitlyAnimatedWidgetState` 我们可以更方便实现一些动画效果,Flutter 里的很多默认动画效果都是通过它实现**。 + +> 另外 `ImplicitlyAnimatedWidget` 模版里,除了 `ImplicitlyAnimatedWidgetState` ,官方还提供了另外一个子类 `AnimatedWidgetBaseState`。 + +事实上 Flutter 里我们常用的 Animated 都是通过 `ImplicitlyAnimatedWidget` 模版实现,如下图所示是 Flutter 里常见的 Animated 分别继承的 State : + +| `ImplicitlyAnimatedWidgetState` | `AnimatedWidgetBaseState` | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![image-20220611194943083](http://img.cdn.guoshuyu.cn/20220619_N4/image6.png) | ![image-20220611195244152](http://img.cdn.guoshuyu.cn/20220619_N4/image7.png) | + +关于这两个 State 的区别,简单来说可以理解为: + +- `ImplicitlyAnimatedWidgetState` 里主要是配合各类 `*Transition` 控件使用,比如: `AnimatedOpacity`里使用了 `FadeTransition` 、`AnimatedScale` 里使用了 `ScaleTransition` ,**因为 `ImplicitlyAnimatedWidgetState` 里没有使用 setState,而是通过触发 RenderObject 的 `markNeedsPaint` 更新界面。** + +- **`AnimatedWidgetBaseState` 在原本 `ImplicitlyAnimatedWidgetState` 的基础上增加了自动 `setState` 的监听**,所以可以做一些更灵活的动画,比如前面我们用过的 `AnimatedPositioned` 和 `AnimatedContainer` 。 + + ![image-20220611164819853](http://img.cdn.guoshuyu.cn/20220619_N4/image8.png) + +其实 `AnimatedContainer` 本身就是一个很具备代表性的实现,如果你去看它的源码,就可以看到它的实现很简单,**只需要在 `forEachTween` 里实现参数对应的 `Tween` 实现即可**。 + +![image-20220611200938194](http://img.cdn.guoshuyu.cn/20220619_N4/image9.png) + +例如前面我们改变的 `width` 和 `height` ,其实就是改变了`Container` 的 `BoxConstraints` ,所以对应的实现也就是 `BoxConstraintsTween` ,**而 `BoxConstraintsTween` 继承了 `Tween` ,主要是实现了 `Tween` 的 `lerp` 方法**。 + +![image-20220611201159887](http://img.cdn.guoshuyu.cn/20220619_N4/image10.png) + +在 Flutter 里 `lerp` 方法是用于实现插值:例如就是在动画过程中,在 `beigin` 和 `end` 两个 `BoxConstraint` 之间进行线性插值,其中 t 是动画时钟值下的变化值,例如: + +> 计算出 100x100 到 200x200 大小的过程中需要的一些中间过程的尺寸。 + +如下代码所示,通过继承 `AnimatedWidgetBaseState` ,然后利用 `ColorTween` 的 `lerp` ,就可以很快实现如下文字的渐变效果。 + +| 代码 | 效果 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![image-20220611203715556](http://img.cdn.guoshuyu.cn/20220619_N4/image11.png) | ![66644](http://img.cdn.guoshuyu.cn/20220619_N4/image12.gif) | + +# 总结 + +最后总结一下,本篇主要介绍了: + +- 利用 `AnimatedPositioned` 和 `AnimatedContainer` 快速实现切换动画效果 +- 介绍 `ImplicitlyAnimatedWidget` 和如何使用 ``ImplicitlyAnimatedWidgetState` / `AnimatedWidgetBaseState` 简化实现动画的需求,并且快速实现自定义动画。 + +那么,你还有知道什么使用 Flutter 动画的小技巧吗? \ No newline at end of file diff --git a/Flutter-N6.md b/Flutter-N6.md new file mode 100644 index 0000000..0214b41 --- /dev/null +++ b/Flutter-N6.md @@ -0,0 +1,234 @@ +# Flutter 小技巧之 Dart 里的 List 和 Iterable 你真的搞懂了吗? + +今天我们介绍关于 `List` 和 `Iterable` 里有趣的知识点 ,你可能会觉得这有什么好介绍,不就是列表吗?但是其实在 Dart 里 `List` 和 `Iterable` 也是很有意思设定,比如有时候我们可以对 `List` 进行 `map` 操作,**如下代码所示,你觉得运行之后会打印出什么内容**? + +```dart +var list = ["1", "2", "3", "4", "5"]; +var map = list.map((e) { + var result = int.parse(e) + 10; + print("######### $result"); + return result; +}); +``` + +**答案是:什么都不会输出,因为通过 `List ` 返回一个 `Iterable` 的操作(如 `map` \ `where`)的都是 Lazy 的**,也就是它们只会在每次“迭代”时才会被调用。 + +比如调用 `toList();` 或者 `toString();` 等方法,就会触发上面的 `map` 执行,从而打印出对应的内容,**那新问题来了,假如我们把下图四个方法都执行一遍,会输出几次 log ?em····答案是 3 次。** + +![image-20220615164227346](http://img.cdn.guoshuyu.cn/20220615_N6/image1.png) + +其中除了 `isEmpty` 之外,其他的三个操作都会重新触发 `map` 方法的执行,那究竟是为什么呢? + +**其实当我们对一个 `List` 进行 `map` 等操作时,返回的是一个 `Iterable` 的 Lazy 对象,而每当我们需要访问里面 value 时, `Iterable` 都会重新执行一遍操作,因为它不会对上次操作的结果进行缓存记录**。 + + 是不是有点懵?这里借用 [fast_immutable_collections ](https://pub.dev/packages/fast_immutable_collections) 作者的一个例子来介绍可能更会清晰,如下代码所示: + +- 我们对同样的数组都调用了 `where` 去获取一个 `Iterable` +- 区别在于在 `evenFilterEager` 里多调用了 `.toList()` 操作 +- 每次 `where` 执行的时候都对各自的 Counter 进行 +1 +- 最后分别调用三次 `length`,输出 Counter 结果 + +```dart +var lazyCounter = 0; +var eagerCounter = 0; + +var lazyOddFilter = [1, 2, 3, 4, 5, 6, 7].where((i) { + lazyCounter++; + return i % 2 == 0; +}); + +var evenFilterEager = [1, 2, 3, 4, 5, 6, 7].where((i) { + eagerCounter++; + return i % 2 == 0; +}).toList(); + +print("\n\n---------- Init ----------\n\n"); + +lazyOddFilter.length; +lazyOddFilter.length; +lazyOddFilter.length; + +evenFilterEager.length; +evenFilterEager.length; +evenFilterEager.length; + +print("\n\n---------- Lazy vs Eager ----------\n\n"); + +print("Lazy: $lazyCounter"); +print("Eager: $eagerCounter"); + +print("\n\n---------- END ----------\n\n"); +``` + +如下图所示,这个例子最终会输出 Lazy: 21 Eager: 7 这样的结果: + +- 因为 `lazyCounter` 每次调用 length 都是直接操作 `Iterable` 这个对象 ,所以每次都会重新执行一次 `where` ,所以 3 * 7 = 21 +- 而 `eagerCounter` 对应的是 `toList();` ,在调用 `toList();` 时就执行了 7 次 `where` ,之后不管调用几次 length 都和 `where` 的 `Iterable` 无关 + +![image-20220615165605170](http://img.cdn.guoshuyu.cn/20220615_N6/image2.png) + +到这里你应该理解了 `Iterable` 的 Lazy 性质的特殊之处了吧? + +那接下来看一个升级的例子,如下代码所示,我们依然是分了 eager 和 lazy 两组做对比,只是这次我们在 `where` 里添加了判断条件,并且做了嵌套调用,那么你觉得输出结果会是什么? + +```dart +List removeOdd_eager(Iterable source) { + return source.where((i) { + print("removeOdd_eager"); + return i % 2 == 0; + }).toList(); +} + +List removeLessThan10_eager(Iterable source) { + return source.where((i) { + print("removeLessThan10_eager"); + return i >= 10; + }).toList(); +} + +Iterable removeOdd_lazy(Iterable source) { + return source.where((i) { + print("removeOdd_lazy"); + return i % 2 == 0; + }); +} + +Iterable removeLessThan10_lazy(Iterable source) { + return source.where((i) { + print("removeLessThan10_lazy"); + return i >= 10; + }); +} + +var list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + +print("\n\n---------- Init ----------\n\n"); + +Iterable eager = removeLessThan10_eager(removeOdd_eager(list)); + +Iterable lazy = removeLessThan10_lazy(removeOdd_lazy(list)); + +print("\n\n---------- Lazy ----------\n\n"); + +print(lazy); + +print("\n\n---------- Eager ----------\n\n"); + +print(eager); +``` + +如下所示,可以看到 : + +- 虽然我们先 `print(lazy);` 之后才输出 `print(eager);` ,但是先输出的还是 `removeOdd_eager` ,因为 Eager 相关的调用里有 `.toList();` ,它在 `removeOdd_eager(list)` 时就执行了,所以会先完整输出 `removeOdd_eager` 之后再完整输出 `removeLessThan10_eager` ,最后在我们 `print(eager);` 的时候输出值 +- lazy 因为是 `Iterable ` ,所以只有被操作时才会输出,并且输出规律是:**输出两次 `removeOdd_lazy` 之后输出一次 `removeLessThan10_lazy`** ,因为从数据源 1-15 上,每两次就符合 `i % 2 == 0;` 的条件,所以会执行 `removeLessThan10_lazy` ,从而变成这样的规律执行 + +```dart +I/flutter (23298): ---------- Init ---------- +I/flutter (23298): +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeOdd_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): removeLessThan10_eager +I/flutter (23298): ---------- Lazy ---------- +I/flutter (23298): +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): removeLessThan10_lazy +I/flutter (23298): removeOdd_lazy +I/flutter (23298): (10, 12, 14) +I/flutter (23298): ---------- Eager ---------- +I/flutter (23298): +I/flutter (23298): [10, 12, 14] +``` + +是不是很觉得,这种时候 `Iterable ` 把事情变得很复杂? 确实在这种复杂嵌套的时候, `Iterable ` 会把逻辑变得很难维护,而官方也表示: + +> 由于 `Iterable ` 可能被多次迭代,因此不建议在迭代器中使用 side-effects 。 + +那了解 `Iterable ` 有什么用?或者说 `Iterable ` 可以用在什么场景?其实还是不少, 例如: + +- 分页,可以确保只有适合用户屏幕渲染时,才执行对应逻辑去加载数据 +- 数据库查询,可以实现使用数据时执行的懒加载效果,并且每次都重新迭代数据请求 + +举个例子,如下代码所示,感受下 `naturalsFunc` 这里 `Iterable` 配合 `Stream` 为什么可以正常: + +```dart +Iterable naturalsFunc() sync* { + int k = 0; + // Infinite loop! + while (true) yield k++; +} + +var naturalsIter = naturalsFunc(); + +print("\n\n---------- Init ----------\n\n"); +print("The infinite list/iterable was created, but not evaluated."); +print("\n\n--------------------\n\n"); +print("\n\n---------- takeWhile ----------\n\n"); +print("It's possible to work with it," + "but it's necessary to add a method to " + "stop the processing at some point"); +var naturalsUpTo10 = naturalsIter.takeWhile((value) => value <= 10); +print("Naturals up to 10: $naturalsUpTo10"); +print("\n\n---------- END ----------\n\n"); +``` + +![image-20220615173602586](http://img.cdn.guoshuyu.cn/20220615_N6/image3.png) + +那到这里你可能会问:**`List` 不也是 `Iterable ` 么,它和 `map `、`where` 、`expand` 等操作返回的 `Iterable ` 又有什么区别** ? + +如果我们看 `List` 本身,你会看到它是一个 abstract 对象,它作为 `Iterable` 的子类,其实一般情况下实现对象会是 dart vm 里的 `_GrowableList`,而 `_GrowableList` 的结构关系如下图所示: + +![image-20220615155944141](http://img.cdn.guoshuyu.cn/20220615_N6/image4.png) + +而 `List` 和其他 `Iterable` 的不同在于在于: + +- `List` 是具有长度的可索引集合,因为其内部 `ListIterator` 是通过 `_iterable.length;` 和 `_iterable.elementAt` 来进行实现 +- 普通 `Iterable` ,如 `map` 操作后的 `MappedIterable` 是按顺序访问的集合,通过 `MappedIterator` 来顺序访问 `iterable` 的元素,也不关心 length + +![image-20220615182431493](http://img.cdn.guoshuyu.cn/20220615_N6/image5.png) + +最后做个总结:本篇的知识点很单一,内容也很简单,就是带大家快速感受下 **`List` 和一般 `Iterable ` 的区别,并且通过例子理解 `Iterable ` 懒加载的特性和应用场景**,这样有利于在开发过程中 `Iterable ` 进行选型和问题定位。 + +如果你还有什么疑惑,欢迎留言评论。 + + + + + diff --git a/Flutter-P3.md b/Flutter-P3.md new file mode 100644 index 0000000..20a1f6e --- /dev/null +++ b/Flutter-P3.md @@ -0,0 +1,96 @@ +在 Flutter 3.0 发布之前,我们通过 [《Flutter 深入探索混合开发的技术演进》](https://juejin.cn/post/7093858055439253534) 盘点了 Flutter 混合开发的历史进程, 在里面就提及了第一代 `PlatformView` 的实现 *VirtualDisplay* 即将被移除,而随着最近 Flutter 3.0 的发布,这个变更正式在稳定版中如期而至,**所以今天就详细分析一下,新的 *TextureLayer* 如何替代 PlatformView** 。 + + + +首先,如下图所示,简单对比 *VirtualDisplay* 和 *TextureLayer* 的实现差异,**可以看到主要还是在于原生控件纹理的提取方式上**。 + +![image-20220516154120729](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image1) + +从上图我们可以得知: + +- 从 *VirtualDisplay* 到 *TextureLayer* , **Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑**; + + + +- 以前 Flutter 中会将 `AndroidView` 需要渲染的内容绘制到 `VirtualDisplays` ,然后在 `VirtualDisplay` 对应的内存中,绘制的画面就可以通过其 `Surface` 获取得到;**现在 `AndroidView` 需要的内容,会通过 View 的 `draw` 方法被绘制到 `SurfaceTexture` 里,然后同样通过 `TextureId` 获取绘制在内存的纹理** ; + +是不是有点懵?简单地说,如下图所示: + +- 现在 `PlatformViewsController` 在加载 `PlatformView` 时, 在 `createForTextureLayer` 方法里会先创建一个 `PlatformViewWrapper` 对象,然后返回一个 `TextureId` 给 Dart ; +- `PlatformViewWrapper` 本身是一个 Android 的 `FrameLayout` ,主要作用就是:通过` addView`添加原生控件,然后**在` draw` 方法里通过 `super.draw(surfaceCanvas);`将 Android View 的 Canvas 替换成 `PlatformView` 创建的 `SurfaceTexture` 的 Canvas** ; +- 在 Dart 层面, `AndroidView` 通过 `TextureId` 告诉 Engine 需要渲染的纹理信息,Engine 提取出前面 `super.draw(surfaceCanvas);` 所绘制的纹理,并渲染出来; + +![image-20220516163607760](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image2) + + + +**这里面的关键就在于 `super.draw(surfaceCanvas);`** 。 + +在 `PlatformView` 创建时,Flutter 会为其创建一个`SurfaceTexture` 用于生成 `Surface`,相当于是在内存里新建了一个画板。 + +而 `PlatformViewWrapper` 里通过 `surface.lockHardwareCanvas()`获取到了这个画板的 Canvas ,也就是 `surfaceCanvas` ,相当于是画笔 。 + +接着 Flutter 通过 `override` 了 `PlatformViewWrapper` 的 `draw(Canvas canvas)`方法,然后在 `super.draw` 时把默认 View 的 Canvas 替换为上面的 `surfaceCanvas`。 + +比如这时候我们需要渲染的原生控件是 `TextView` ,**因为此时 `TextView` 是 `PlatformViewWrapper` 的子控件,所以当它绘制时,使用的画笔就会是 `surfaceCanvas` ,而它的界面效果就会被绘制到对应 Id 的 `SurfaceTexture` 里**。 + +所以在新流程里,原生控件同样是渲染到内存,然后通过 Id 去获取纹理数据,但是对比 VirtualDisplay 它更直接,因为是直接位置到内存纹理而不是通过虚显,并且这里有个关键内容: + +> **使用的是 `lockHardwareCanvas()` 而不是 `lockCanvas()`, `lockHardwareCanvas()` 需要 API 23 以上才支持,因为它支持硬件加速,而不是像 `lockCanvas` 一样需要频繁的 CPU 拷贝,从而提高了性能。** + +那我们知道,在以前的 `VirtualDisplays` 实现里,除了性能问题,还有控件的触摸问题,因为 `AndroidView` 其实是被渲染在 `VirtualDisplay` 中 ,而每当用户点击看到的 `"AndroidView"` 时,其实他们就真正”点击的是正在渲染的 `Flutter` 纹理 ,用户产生的触摸事件是直接发送到 Flutter View 中,而不是他们实际点击的 `AndroidView`。 + +而在 *TextureLayer* 的实现里,**虽然控件同样是被绘制到内存,但是 `PlatformViewWrapper` 是真实存在布局里的** 。 + +什么意思呢? + +如下图所示,是将两个 `TextView` 通过 *TextureLayer* 的方式添加到 Flutter 里 ,然后我们通过 Android Studio 的 Layout Inspector 查看,可以看到 `FlutterView` 下会有两个 `PlatformViewWrapper` ,并且它们都有一个 `TextView` 的子控件。 + +![image-20220516112123711](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image3) + +**此时因为 `TextView` 的子控件的 Canvas 被 Flutter 给替换了,所以在画面上看不到渲染内容,但是它们所在的位置依然可以接受点击事件**。 + +所以在 `PlatformViewWrapper` 中,它 override 了 `onTouchEvent` 方法,并且将对应的 `MotionEvent` 进行封装,然后分发到 Flutter 的 Dart 层进行处理。 + +> 当然,此时 `PlatformViewWrapper` 的位置和大小 ,是通过 Dart 层的 `AndroidView` 传递过来的信息进行定位,而 `PlatformViewWrapper` 的位置其实和渲染效果没有关系,即使 `PlatformViewWrapper` 不在正常位置,画面也可以正常渲染,它影响的主要还是触摸事件的相关逻辑。 + +值得注意的是, **`PlatformViewWrapper` 里的 ` onInterceptTouchEvent` 返回了 true ,也就是触摸事件会被它拦截,而不会传递,避免了 `FlutterView` 收到干扰**。 + +![image-20220516172819574](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image4) + +这里刚好有人提了一个问题,如下图所示: + +> "从 Layout Inspector 看 `FlutterWrapperView` 是在 `FlutterSurfaceView` 上方,为什么点击 Flutter button 却可以不触发 native button的点击效果?"。 + + +| 图1 | 图2 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| ![image.png](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image5) | ![](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image6) | + +> 这里简单解释一下: +> - 1、首先那个 Button 并不是真的被摆放在那里,而是通过 `PlatformViewWrapper` 的 `super.draw` 绘制到 surface 上的,所以在那里的是 `PlatformViewWrapper` ,而不是 Button +> - 2、 `PlatformViewWrapper` 里 `onInterceptTouchEvent` 做了拦截,`onInterceptTouchEvent` 这个事件是从父控件开始往子控件传,因为拦截了所以不会让 Button 直接响应,然后在 `PlatformViewWrapper` 的 `onTouchEvent` 其实是做了点击区域的分发,响应分发到了 `AndroidTouchProcessor` 之后,会打包发到 `_unpackPointerDataPacket` 进入 Dart +> - 3、 在 Dart 层的点击区域,如果没有 Flutter 控件响应,会是 `_PlatformViewGestureRecognizer` -> `updateGestureRecognizers` -> `dispatchPointerEvent` -> `sendMotionEvent` 发送回原生层 +> - 4、回到原生 `PlatformViewsController` 的 `createForTextureLayer` 里的 `onTouch` ,执行 `view.dispatchTouchEvent(event);` + + +另外 `PlatformViewWrapper` 还提供了焦点相关的处理逻辑,通过接口将焦点的变化状态返回给 Dart 层。 + +![image-20220516173618441](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image7) + +最后, `PlatformViewWrapper` 里还有一个小兼容处理:就是在 Android Q 上 `SurfaceTexture` 需要绘制完上一帧之后,才能绘制下一帧。 + +![image-20220516174428087](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image8) + +简单地说,具体流程为: + +- 所以当 Engine 每次绘制时,就会触发 `onFrameComsumed` 去对 `pendingFramesCount` 进行 -1 操作; +- 每次有新的 `SurfaceTexture` 或者 `draw(Canvas canvas)` 调用,就对 `pendingFramesCount` 进行 +1 操作; + +通过 `pendingFramesCount` 的计数方式,当 `pendingFramesCount.get() <= 0L` 才进行 `Surface` 绘制,保证了 Android Q 上 `SurfaceTexture` 每次提交绘制都是最后一帧的画面。 + +可以看到 ,新的 *TextureLayer* 实现更简单直接,实现了在性能提高的同时,简化了实现的复杂度,同时也弥补了 *VirtualDisplay* 的一些缺陷。 + +最后,从 Flutter 3.0 源码上看,**社区有打算移除 *HybirdComposition* 的计划,但是这无疑是一个涉及面比较大的 break change ,最终是否能够通过还不得而知**,而从我个人角度出发,我是觉得 *HybirdComposition* 在某些场景还有存在的必要,如果想详细了解 *HybirdComposition* ,可以参考 [《Flutter 深入探索混合开发的技术演进》](https://juejin.cn/post/7093858055439253534#heading-8) + +![image-20220516180731371](http://img.cdn.guoshuyu.cn/20220627_Flutter-P3/image9) \ No newline at end of file diff --git a/Flutter-TL.md b/Flutter-TL.md new file mode 100644 index 0000000..982ab6c --- /dev/null +++ b/Flutter-TL.md @@ -0,0 +1,102 @@ + + +# Flutter 从 TextField 安全泄漏问题深入探索文本输入流程 + +Flutter 的 `TextField` 相信大家都很熟悉,作为输入控件 `TextField` 经常出现在需要登录的场景,例如在需要输入密码的 `TextField` 上配置 `obscureText: true` ,这时候就会如下图所示,输入框呈现加密显示的状态。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image1) + +而在登录成功之后,登录页面一般都会随之被销毁,连带着用户的账号和密码数据也应该会被回收,但是事实上有被回收吗? + + + +## 一、CWE-316 + +事实上如果你使用 `TextField` 作用密码输入框,这时候你很可能会在**安全合规**中遇到类似 CWE-316 的警告,主要原因在于:**Flutter 在进行文本输入时,和原生平台通信过程中,会有明文的文本内容残留**。 + +复现这个问题很简单,首先我们需要一个能够读取 App 运行时内存数据的工具,这里推荐使用 [apk-medit](https://github.com/aktsk/apk-medit) ,具体使用流程为: + +- 下载 [apk-medit](https://github.com/aktsk/apk-medit/releases/) 的压缩包,解压得到 `medit` 可执行文件; +- usb 调试链接上手机,无需 root ,执行 `adb push medit /data/local/tmp/medit` 将可执行文件传输到手机上; +- 执行 `adb shell` 进入手机命令后模式; +- 执行 `run-as ` ,其中 target-package-name 就是你的包名; +- 执行 `cp /data/local/tmp/medit ./medit` 拷贝可执行文件; +- 执行 `./medit` 进入内存检索模式; + +成功之后可以看到如下图所示,进入到了待命的状态: + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image2) + +这时候我们在密码输入框输入 abcd12345 ,然后在终端 `find abcd12345` 可以看到在 `String` 类别下找到 7 个相关的内存数据。 + +![image-20220426115105174](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image3) + +之后我们通过 `TextField` 的 `controller` 清空输入文本,销毁当前页面,跳转到空白页面下后,同时在 Flutter devTool 上主动点击 GC 清理数据,最后再回到终端执行 `find abcd12345` ,结果如下图所以: + +![image-20220426115504463](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image4) + +可以看到这时候还有 5 个相关数据存在内存,这里挑选一个地址,如 `0x7194a57b` 执行 `dump` 命令: `dump 0x7194a500 0x7194a5ff` ,结果如下图所示,**可以看到此时的密码是以 map 格式存在,并且长时间都不会被回收或者销毁**。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image5) + +**这个问题目前在 Android、iOS、Linux 等平台都普遍存在,那这个问题是从哪里来的**? 这就需要聊到 Flutter 里的文本输入实现流程。 + +## 二、文本输入流程 + +Flutter 作为跨平台框架,它的文本内容输入主要是依赖平台的通道实现,例如在 Android 上就是通过 `InputConnection ` 相关的体系去实现。 + +在 Android 上,**当输入法要和某些 View 进行交互时,系统会通过` View` 的 `onCreateInputConnection` 方法返回一个 `InputConnection` 实例给输入法用于交互通信**,开发者可以通过 override `InputConnection` 上的一些方法来进行拦截某些输入或者响应某些 key 逻辑等操作,例如: + +> Android SDK 里提供的 `EditText` 控件之所以支持文本输入,也是因为它继承的父类 `TextView` 实现了对应的 `EditableInputConnection` ,并复写了` View` 的 `onCreateInputConnection` 方法。 + +![image-20220426084518804](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image6) + +在 Flutter 上,**`FlutterView` 同样 override 了 `onCreateInputConnection` 方法,并实现了 `InputConnectionAdaptor` 作为交互** ,这里先简单介绍一些后面用到的对象: + +- **InputConnectionAdaptor** : `InputConnection` 的实现,用于输入法和 Flutter 之间的通信交互,内部持有: `TextInputChannel` 、 `ListenableEditingState` 、`InputMethodManager` 、`KeyboardManager` 等对象; +- **TextInputChannel** : `MethodChannel` 的封装对象, 主要和 Dart 进行交互通信,并实现一些逻辑; +- **InputMethodManager** :Android 系统的键盘管理对象,例如通过它显示/隐藏键盘,或者配置一些键盘特性; +- **ListenableEditingState**:用于保存当前编辑状态,如文本内容、选择范围等等,因为 `InputConnection` 会需要一个 `Editable` 接口,而它就是 `Editable` 接口的子类,Andorid framework 里键盘输入的内容和状态会通过 `Editable` 接口进行操作; +- **TextInputPlugin** : 它的作用类似于 FlutterPlugin 的作用,持有 `TextInputChannel` 和 `InputMethodManager` 实现一些输入相关逻辑,**同时本身也实现了 `ListenableEditingState.EditingStateWatcher` 接口,该接口当有文本输入时会被调用**; + +简单介绍完这些对象的作用,我们回到文本输入的流程上,当用键盘输入完内容时,文本输入内容会进入到 `InputConnectionAdaptor` 的 `endBatchEdit` ,然后如下图所示: + +- 键盘输入的内容会保存在 `ListenableEditingState` 里(源码里的 `mEditable` 参数); +- 之后会通知到 `TextInputPlugin` 去格式化数据并传入 `TextInputChannel` ; +- 接着通过 `TextInputChannel` 把数据封装在 Map 格式,然后通过 invoke 到 `TextInputClient.updateEditingState` 的 dart 方法上; +- Dart 层面接收到 Map 内容之后,将输入内容更新到 `TextEditingValue` 上,从而渲染出输入的文本; + +![image-20220426131155331](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image7) + + + +可以看到,整个流程主要是:**通过 `InputConnectionAdaptor` 和输入法交互之后得到输入内容和状态,然后将数据封装为 Map 传给 Dart 层,Dart 层解析显示内容**。 + +那回到上面的 CWE-316 的问题,可以看到此时内存留残留的明文密码正是 `TextInputClient.updateEditingState` ,也就是原生平台传给 Dart 层的 Map 数据,这部分数据在传递之后没有被回收,导致残留在内容,出现泄漏。 + +![image-20220426134842594](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image8) + +事实上关于改问题,在 Flutter 的 [#84708](https://github.com/flutter/flutter/issues/84708) issues 上有过讨论,虽然官方将其定义为 P3 的状态,但是从回复上可以看到,意思大概是: **CWE-316 问题看起来更多是被误导,因为如果第三方可以随意访问到你的设备数据,那其实无论用什么方式都很难避免所谓的泄漏。** + +![image-20220426135249882](http://img.cdn.guoshuyu.cn/20220627_Flutter-TL/image9) + +另外从目前的 Dart 设计上看, Dart `String` 对象是不可变的,一旦明文 `String` 进入 Dart heap,就无法确保它何时会被清理,而且即使在 String 被 GC 之后,它曾经占用的内存也将保持不变,直到整个区域被清空并交还给操作系统,或在该地址分配了一个新对象,这时候才可能会被完全清除。 + +另外这里额外补充两个 `InputConnectionAdaptor` 的知识点:`performEditorAction` 和 `sendKeyEvent ` 。 + +- **performEditorAction** : 当输入法上一些特别的 Key 如 `IME_ACTION_GO`、`IME_ACTION_SEND` 、 `IME_ACTION_DONE` 这些 Key 被触发是时,会直接通过 `TextInputChannel` 将 code 发送到 Dart ; +- **sendKeyEvent** : 当某些特殊按键输入时会被回调,例如点击退格键时,但是这个取决于输入的不同,例如小米安全键盘输入法的退格键就不会触发,但是小米安全键盘输入法的数字 key 就会触发该回调; + + + +## 三、最后 + + + +所以就目前版本的情况来看,**只要是使用了 `TextField` ,或者说 `EditableText` ,那么传输过程的 Map 残留问题可能会一直存在**。 + +当然,**如果你只是使用 String 而不是使用 `EditableText` ,那么 Dart 上类似 typed data 或者 ffi pointers 的能力,一定程度可以解决此类的问题**。 + + + +如果针对 `TextField` 的 CWE-316 你还有什么想法,欢迎留言讨论交流~ \ No newline at end of file diff --git a/Flutter-Web-T.md b/Flutter-Web-T.md new file mode 100644 index 0000000..bdb181f --- /dev/null +++ b/Flutter-Web-T.md @@ -0,0 +1,662 @@ +# 大前端时代的乱流:带你了解最全面的 Flutter Web + +Flutter Web 稳定版本发布至今也有一年多了,经过这一年多的发展,今天就让我们来看看作为大前端时代的乱流,Flutter Web 究竟有什么不同之处,**本篇分享主要内容是目前 Flutter 下少有较为全面的 Web 内容**。 + +> 本篇来自本人在《T技术沙龙-大前端时代的挑战与机遇(深圳场)》的线下技术分享。 + +## 一、起源与实现 + +说起 Flutter 的起源就很有意思,大家都知道早期 Flutter 最先支持的平台是 Android 和 iOS ,至今最核心的维护平台依然是 Android 和 iOS ,**但是事实上 Flutter 其实起源于前端团队**。 + +> Flutter 来源于前端 Chrome 团队,起初 Flutter 的创始人和整个团队几乎都是来自 Web,在 Flutter 负责人 Eric 的相关访谈中, Eric 表示 Flutter 来自 Chrome 内部的一个实验,他们把一些乱七八糟的 Web 规范去掉后,在一些内部基准测试的性能居然能提升 20 倍,因此 Google 内部就开始立项,所以 Flutter 出现了。 + +另外前端的同学应该知道, Dart 起初也是为了 Web 而生,事实上在 Dart 诞生至今也有 10 年了,所以**可以说 Flutter 其实充满了 Web 的基因**。 + +但是作为从 Web 里诞生的框架,和 React Native/ Weex 不同的是,前者是先有了 Web 下的 React 和 Vue 实现之后才有的客户端支持,而对于 Flutter 则是反过来,先有客户端实现之后才支持 Web 平台,**这里其实可以和 Weex 做个简单对照**。 + +Weex 作为曾经闪耀过的跨平台框架,它同样支持 Android 、iOS 和 Web 三个平台,在 Android 和 iOS 上 Weex 和 React Native 差异性不大,在 Web 上 Weex 则是删减版的 Vue 支持,而由于 API 和平台差异性的问题,Weex 在 Web 上的支持体验一直不是很好: + +> 因为 Weex 需要依赖平台控件实现渲染,导致一个 Text 控件需要兼顾 Android 、iOS 和 Web 上原生平台接口的逻辑,从而出现各种由于耦合带来的兼容性问题。 + +而 Flutter 实现更为特别,通过 Skia 实现了独立的渲染引擎之后,在 Android 和 iOS 上控件几乎就与平台无关,所以 Flutter 上的控件可以做到独立且不同平台上渲染一致的效果。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image1) + +但是回到 Web 上又有些特殊,首先 Web 平台完全是 html / js / css 的天下,并且 Web 平台需要同时兼顾 PC 和 Mobile 的不同环境,这就让 Flutter Web 成了 Flutter 所有平台里“最另类又奇葩”的落地。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image2) + +首先 Flutter Web 和其他 Flutter 平台一样共用一套 Framework ,理论上绝大多数的控件实现都是通用的,当然如果要说最不兼容的 API 对象,那肯定就是 `Canvas` 了,这其实和 Flutter Web 特殊的实现有关系,后面我们会聊到这个问题。 + +而由于 Web 的特殊场景,**Flutter Web 在“几经周折”之后落地了两种不同的渲染逻辑:html 和 canvaskit** ,它们的不同之余在于: + +#### html + +- 好处:html 的实现更轻量级,渲染实现基本依赖于 Web 平台的各种 HTMLElement ,特别是 Flutter Web 下定义的各种 `` 实现,可以说它更贴近现在的 Web 环境,所以有时候我们也称呼它为 `DomCanvas` ,**当然随着 Flutter Web 的发展这个称呼也发了一些变化,后续我们会详细讲到这个**。 + +- 问题:html 的问题也在于太过于贴近 Web 平台,这就和 Weex 一样,贴近平台也就是耦合于平台,事实上 `DomCanvas` 实现理念其实和 Flutter 并不贴切,也导致了 Flutter Web 的一些渲染效果在 html 模式下存在兼容问题,特别是 `Canvas` 的 API 。 + +#### canvaskit + +- 好处:canvaskit 的实现可以说是更贴近 Flutter 理念,因为它其实就是 Skia + WebAssembly 的实现逻辑,能和其他平台的实现更一致,性能更好,比如滚动列表的渲染流畅度更高等。 + +- 问题:很明显使用 WebAssembly 带来的 wasm 文件会导致体积增大不少,Web 场景下其实很讲究加载速度,而在这方面 wasm 能优化的空间很小,并且 WebAssembly 在兼容上也是相对较差,另外 skia 还需要自带字体库等问题都挺让人头痛。 + +**默认情况下 Flutter Web 在打包渲染时会把 html 和 canvaskit 都打包进去,然后在 PC 端使用 canvaskit 模式,在 mobile 端使用 html 模式** ,当然你也可以在打包时通过 `flutter build web --web-renderer html --release ` 之类的配置强行指定渲染模式。 + +既然这里我们讲到了 Flutter Web 的打包构建,那就让我们先从构建打包角度开始来深入介绍 Flutter Web 。 + +## 二、构建和优化 + +**Flutter Web 虽说是和其他平台共用一个 framework ,但是它在 dart 层开始就有一套自己特殊的 engine 实现**,并且这套实现是独立于 framework 的一套特殊代码。 + +所以在 Flutter Web 打包时,会把默认的 `/flutter/bin/cache/lib/_engine` 变成了 `flutter/bin/cache/flutter_web_sdk/lib/_engine` 的相关实现,这是因为 Flutter Web 在 framework 之下的 engine 需要一套特殊的 API。 + +> 下图右侧构建是指定 web 的打包路径,和左边默认时的对比。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image3) + +同样下图所示,可以看到 web sdk 里会有如 html 、 canvaskit 这样不同的实现,甚至会有一个特殊的 text 目录,这是因为在 web 上对于文本的支持是个十分复杂的问题。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image4) + +那到这里我们知道了在 ` _engine` 层面,Flutter Web 有着自己一套独立的实现,那构建之后的产物是什么样的情况呢? + +如下图所示是 GSY 的一个简单的开源示例项目,在部署到服务器后可以看到,默认情况下在不做任何处理时, 在 PC 端打开后会使用 canvaskit 渲染,主要会有: + +- 2.3 MB 的 `main.dart.js` ; +- 2.8 MB 的 `canvaskit.wasm` ; +- 1.5 MB 的 `MaterialIcons-Regular.otf `; +- 284 kB 的 `CupertinoIcons.ttf` ; + + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image5) + + + +可以看到这些文件占据了 Flutter Web 编译后产物的大部分体积,并且从大小上看确实让人有些无法接受,因为示例项目的代码量并不大,结构也不复杂,这样的体积肯定十分影响加载速度。 + +所以我们首先考虑在 html 和 canvaskit 两种渲染模式中先选定一种,出于实用性考虑,结合前面的对比情况,**选用 html 渲染模式在兼容性和可优化上会更友好,所以这里优化的第一步就是先指定 html 模式作为渲染引擎**。 + + + +### 开始优化 + + + +首先可以看到 CupertinoIcons.ttf 这个矢量图标文件,虽然默认创建项目时会通过 `cupertino_icons` 被添加到项目里,但是由于我们不需要使用,所以可以在 yaml 文件里去除。 + +之后通过运行 `flutter build web --release --web-renderer html` 后,可以看到使用 html 模式加载后的产物很干净,而需要优化的体积现在主要在 main.dart.js 和 MaterialIcons-Regular.otf 上。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image6) + +虽然在项目中我们会使用到 MaterialIcons 的一些矢量图标,但是每次加载都要全量加载一个 1.5 MB 的字体库文件显然并不符合逻辑,**所以在 Flutter 里官方提供了 ` --tree-shake-icons` 的命令帮助我们优化这部分的内容**。 + +但是不幸的是,如下图所示,在当前的 2.10 版本下该配置运行会有 bug ,而不幸中的万幸是,在原生平台的编译中 `shake-icons` 行为是可以正常执行。 + +![image-20220318160151235](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image7) + +所以我们可以先运行 `flutter build apk` ,然后通过如下命令,将 Android 上已经 shake-icons 的 `MaterialIcons-Regular.otf` 资源复制到已经编译好的 web/ 目录下。 + +```sh +cp -r ./build/app/intermediates/flutter/release/flutter_assets/ ./build/web/assets +``` + +再次打包后可以看到,经过优化后 `MaterialIcons-Regular.otf` 资源如今只剩下 3.2 kB ,那解下来就是考虑针对 2.2 MB 的 main.dart.js 进行优化处理。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image8) + +要优化 main.dart.js ,我们就要讲到 Flutter 里的 `deferred-components` , 在 Flutter 里可以通过把控件定义为 “deferred component” 来实现控件的懒加载,而这个行为在 Flutter Web 上被编译之后就会变成多个 `*part.js` 文本,原理上就是对 main.dart.js 进行拆包。 + +举个例子,首先我们定义一个普通的 Flutter 控件,按照正常的控件进行实现就可以。 + +```dart +import 'package:flutter/widgets.dart'; +class DeferredBox extends StatelessWidget { + DeferredBox() {} + @override + Widget build(BuildContext context) { + return Container( + height: 30, + width: 30, + color: Colors.blue, + ); + } +} +``` + +在需要的地方 `import` 对应控件然后添加 `deferred as box ` 关键字,之后在适当时机通过 ` box.loadLibrary()` 加载控件,最后通过 `box.DeferredBox()` 渲染。 + +```dart +import 'box.dart' deferred as box; +class MainPage extends StatefulWidget { + @override + _MainPageState createState() => _MainPageState(); +} +class _MainPageState extends State { + @override + void initState() { + super.initState(); + } + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: box.loadLibrary(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + return box.DeferredBox(); + } + return CircularProgressIndicator(); + }, + ); + } +} +``` + +当然,这里还需要额外在 ymal 文件里添加 `deferred-components` 来制定对应的 libraries 路径。 + +``` +deferred-components: + - name: crane + libraries: + - package:gsy_flutter_demo/widget/box.dart +``` + +回归到上面的 GSY 示例项目中,通过相对极端的分包实现,这里把 GSY 示例里的每个页面都变成一个独立的 懒加载页面,然后在页面跳转时再加载显示,最终打包部署后如下图所示: + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image9) + +可以看到拆分之后 main.dart.js 从 2.2 MB 变成了 1.6 MB ,而其他内容通过 deferred components 变成了各个 part.js 的独立文件,并且只在点击时才动态下载对应的 part.js 文件,**但是此时的 main.dart.js 依旧并不小,而官方提供的能力上已经没有太多优化的余地**。 + +> 关于 `deferred-components` 会遇到的问题,可以参考 [《一个编译问题带你了解 Flutter Web 的打包构建和分包实现》](https://juejin.cn/post/7079062175532187656) + +在这里可以通过前端的 `source-map-explorer` 工具去分析这个文件,首先在编译时要添加 `--source-maps` 命令,这样在打包时会生成 main.dart.js 的 source map 文件,然后就执行 `source-map-explorer main.dart.js --no-border-checks ` 生成对应的分析图: + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image10) + +这里只展示能够被 mapped 的部分,可以看到 700k 几乎就是 Flutter Web 整个 framewok + engine + vm 的大小,而这部分内容其实可以优化的空间并不大,尽管会有一些如 ` kIsWeb` 的冗余代码,但是其实可以调整的内容并不多,大概有 36 处可以调整和删减的地方,实质上打包时 Flutter Web 也都有相应的优化压缩处理,所以这部分收益并不高。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image11) + +另外,如下图所示是两种不同 web rendder 构建后代码上的差异,可以看到 html 和 canvaskit 单独构建后的 engine 代码结构差异性还是很大的。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image12) + + + +而如果你在编译时时默认的 auto 模式,就会看到 html 和 canvaskit 的代码都会打包进去,所以相对的 main.dart.js 也会增加一些。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image13) + +那还有什么可以优化的地方吗?还是有的,通过外部手段,例如通过在部署时开启 gzip 或者 brotli 压缩,如下图所示 ,开始 gzip 后大概可以让 main.dart.js 下降到 400k 左右 。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image14) + +另外也有在 index.html 里增加 loading 效果来做等待加载过程的展示,例如: + +```html + + + + + gsy_flutter_demo + + + +
+
+
+ + + + +``` + +所以大致上以上这些就是今天关于 Flutter Web 上产物体积的优化,总结起来就是: + +- 去除无用的 icon 引用; +- 使用 `tree-shake-icons` 优化引用矢量图库; +- 通过 `deferred-components` 实现懒加载分包; +- 开启 `gzip` 等压缩算法压缩 `main.dart.js` ; + +## 三、渲染 + +讲完构建,最后我们聊聊渲染,Flutter Web 的渲染在 Flutter 里是十分特殊,前面我们说过它自带了两种渲染模式,而我们知道 Flutter 得设计理念里,所有的控件都是通过 Engine 绘制出来,如果这时候你去 framework 里看 `Canvas` 的实现,就会发现它其实继承的是 `NativeFieldWrapperClass1` : + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image15) + + + +**`NativeFieldWrapperClass1` 也就是它的逻辑是由不同平台的 Engine 区分实现**,其中编译后的 Flutter Web 上的 `Canvas` 代码应该是继承如下所示的结构: + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image16) + +可以看到在 Flutter Web 的 Canvas 里会根据逻辑判断是使用 `CanvasKitCanvas` 还是 `SurfaceCanvas` ,**而相对于直接使用 skia 的 `CanvasKitCanvas` ,更贴近 Web 平台的 `SurfaceCanvas` 在实现的耦合复杂度上会更高**。 + +首先如下图所示是 Flutter Web 里 Canvas 的大致结构,而接下来我们要聊的主要也是集中在 `SurfaceCanvas` 上,*为什么 `SurfaceCanvas` 层级会这么复杂,它们又是怎么分配绘制,接下来就让深入揭秘它们的规则*。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image17) + + + + + +先看例子,如下图所示,可以看到在 html 渲染模式下, Flutter Web 是有一大堆自定义的 `` 标签实现渲染,并且在一个长列表中,标签会被控制在一个合适的数量,在滚动时动进行动态切换渲染。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image18) + +如果这时候我们放慢去看细节,如下动图所示,可以看到当 item 处于不可见时 `` 里其实并没有内容,而当 Item 可见之后,`` 下会有 `` 标签把文字绘制出来。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image19) + +**看到一个重点没有?在这里的文本为什么是由 `` 标签绘制而不是 `

` 标签之类的呢**?这就是我们重点要讲的 `SurfaceCanvas` 渲染逻辑。 + +在 Flutter Web 的 `SurfaceCanvas` 里,文本绘制一般都会是以这样的情况出现,基本都是从 picture 开始进入绘制流程: + + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image20) + +那么在对应的 `picture.dart` 的代码实现里可以看到,如下关键代码所示,当` hasArbitraryPaint` 为 `true` 时就会进入到 `BitmapCanvas` 的逻辑,不然就会使用 `DomCanvas` 。 + +```dart +void applyPaint(EngineCanvas? oldCanvas) { + if (picture.recordingCanvas!.renderStrategy.hasArbitraryPaint) { + _applyBitmapPaint(oldCanvas); + } else { + _applyDomPaint(oldCanvas); + } +} +``` + +那么这里有两个问题:*`BitmapCanvas` 和 `DomCanvas` 的区别是什么?`hasArbitraryPaint` 的判断逻辑是什么*? + +1、首先 `BitmapCanvas` 和 `DomCanvas` 的最大的区别就是: + +- `DomCanvas` 会通过创建标签来实现绘制,比如文本利用 `p` + `span` 标签进行渲染; +- `BitmapCanvas` 会考虑优先使用` canvas` 渲染,如果场景需要再使用标签来实现绘制; + +2、在 web sdk 里 `hasArbitraryPaint` 参数默认是 `false` ,但是在需要执行以下这些行为时就会被设置为 `true` ,而这些调用上可以看出,其实大部分时候的绘制逻辑是会先进入到 `BitmapCanvas` 里。 + + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image21) + +回到前面的文本问题上,**在 Flutter 的文本绘制一般都是通过 `drawParagraph` 实现,所以理论上只要有文本存在,就会进入到 `BitmapCanvas` 的绘制流程**,那么目前看来这个结论符合上面 Item 里文本是使用 `canvas` 绘制的预期。 + +*那 Flutter 里对于文本,在 `BitmapCanvas` 又是何时使用`canvas` 何时使用 `p`+`span` 标签呢*? + +我们先看如下代码,运行后效果如下图所示,可以看到此时的文本是直接使用 `canvas` 渲染的,这个结果符合我们目前的预期。 + +```dart +Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: Container( + child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + ), + ), + ), + ), +) +``` + +![image-20220323103644032](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image22) + +接下来给这段代码加上一个红色背景,运行后可以看到,此时的文本变成了 `p`+`span` 标签,并且红色的背景是通过 `draw-rect` 标签实现,层级里并没有 `canvas` ,这又是为什么呢? + +```dart +Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.red, + ), + child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + ), + ), + ), + ), +) +``` + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image23) + +这里就需要先讲到 `BitmapCanvas` 的 `drawRect` 实现,如下关键代码所示,在 `drawRect` 时,如果在满足 `_useDomForRenderingFillAndStroke` 这个函数条件的情况下,就会x通过 `buildDrawRectElement`的方式实现渲染,也就是使用 `draw-rect` 标签而不是 `canvas` ,所以我们需要先分析这个函数的判断逻辑。 + +```dart +@override +void drawRect(ui.Rect rect, SurfacePaintData paint) { + if (_useDomForRenderingFillAndStroke(paint)) { + final html.HtmlElement element = buildDrawRectElement( + rect, paint, 'draw-rect', _canvasPool.currentTransform); + _drawElement( + element, + ui.Offset( + math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)), + paint); + } else { + setUpPaint(paint, rect); + _canvasPool.drawRect(rect, paint.style); + tearDownPaint(); + } +} +``` + +如下代码所示,可以看到这个函数有很多的判断条件,而得到 `true` 的条件就是满足其中三大条件之一即可,下述表格里大致描述了每个条件的所代表的意义。 + +```dart + bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) => + _renderStrategy.isInsideSvgFilterTree || + (_preserveImageData == false && _contains3dTransform) || + ((_childOverdraw || + _renderStrategy.hasImageElements || + _renderStrategy.hasParagraphs) && + _canvasPool.isEmpty && + paint.maskFilter == null && + paint.shader == null); + +``` + +| isInsideSvgFilterTree | 例如有 ShaderMask 或者 ColorFilter 的时候为 `true` | +| -------------------------------- | ------------------------------------------------------------ | +| _preserveImageData | 一般是在 toImage 的时候才会为 `true` | +| _contains3dTransform | transformKind == TransformKind.complex 的时候,也就是矩阵包含缩放、旋转、z平移或透视变换 | +| _childOverdraw | 有 _drawElement 或者 drawImage 的时候,大概就是使用了标签渲染之后,需要切换画布 | +| _renderStrategy.hasImageElements | 有图片绘制的时候,用 Image 标签的情况 | +| _renderStrategy.hasParagraphs | 有文本需要绘制的时候 | +| _canvasPool.isEmpty | 简单说就是 canvas == null 的时候 | +| paint.maskFilter == null | 简单说就是 Container 等控件没有配置 shadow 的时候 | +| paint.shader == null | 简单说就是 Container 等控件没有配置 gradient 的时候 | + +大概流程也如图所示,前面绘制红色背景时并没有添加什么特殊配置,所以会进入到 `_drawElement` 的逻辑,可以看到针对不同的渲染场景,`BitmapCanvas` 会采取不一样的绘制逻辑,那为什么前面多了红色背景就会导致文本也变成标签呢? + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image24) + +这是因为在 `BitmapCanvas` 如果有使用标签构建,也就是 `_drawElement` 的时候,就会执行一个 `_closeCurrentCanvas` 函数,该函数会把 `_childOverdraw` 设置为` true` ,并且清空 `_canvasPool` 里的 canvas 。 + +所以我们看 `drawParagraph` 的实现,如下所示代码,可以看到由于 `_childOverdraw` 是 true 时, 文本会采用 `Element` 来绘制文本。 + +```dart +@override +void drawParagraph(EngineParagraph paragraph, ui.Offset offset) { + ···· + if (paragraph.drawOnCanvas && _childOverdraw == false && + !_renderStrategy.isInsideSvgFilterTree) { + paragraph.paint(this, offset); + return; + } + ···· + final html.Element paragraphElement = + drawParagraphElement(paragraph, offset); + + ···· +} +``` + +而在 `BitmapCanvas` 里,有三个操作会触发 `_childOverdraw = true` 和 `_canvasPool Empty `: + +- _drawElement +- drawImage/drawImageRect +- drawParagraph + +所以先总结一下,结合前面的流程图,我们可以简单认为:**在没有 maskFilter(shadow) 和 shader(gradient )的情况下,只要触发了上述三种情况,就会使用标签绘制。** + +是不是感觉有点乱? + +不怕,先接着继续看新的例子,在原本红色背景实现的基础上,这里给 `Container` 增加了 shadow 用于配置阴影,运行之后可以看到,不管是背景色或者文本又都变成了 `canvas` 渲染的情况。 + +```dart +Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.red, + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 4.0, + offset: Offset(2, 2)) + ], + ), + child: Text( + "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + ), + ), + ), + ), + ) +``` + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image25) + +结合前面的流程看这是符合预期,因为此时带有 `boxShadow` 参数,该参数会在绘制时通过 `toPaint` 方法转化为 `maskFilter` ,所以在 `maskFilter != null` 的情况下,流程不会进入到 `Element` 的判断,所以使用 `canvas` 。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image26) + +继续前面的例子,如果这时候我们再加一个 `ColorFiltered` 控件,前面表格说过,有 ShaderMask 或者 ColorFilter 的时候,`sInsideSvgFilterTree ` 参数就会是 true ,这时候渲染就会直接进入使用 `Element` 绘制而无视其他条件如 `BoxShadow` ,从运行结果上看也是如此。 + +```dart +Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: ColorFiltered( + colorFilter: ColorFilter.mode(Colors.yellow, BlendMode.hue), + child:Container( + decoration: BoxDecoration( + color: Colors.red, + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 4.0, + offset: Offset(2, 2)) + ], + ), + child: Text( + "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + ), + ), + ), + ), + ), + ) +``` + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image27) + + + +可以看到此时变成了两个 `draw-rect` 和 `p` 标签的绘制,为什么会有这样的逻辑,因为一些浏览器,例如 iOS 设备上的 Safari, 它不会把 svg filter 等信息传递给 `canvas `,如果继续使用 `canvas` 就会如 shader mask 等无法正常渲染,详细可见 :[#27600]( https://github.com/flutter/engine/pull/27600) 。 + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image28) + +继续这个例子,如果此时不加 `ColorFiltered` ,而是给 `Container` 添加一个 `transform` ,运行后可以看到还是 `draw-rect` 和 `p` 标签的实现,因为此时的 `transform ` 是属于 `TransformKind.complex ` 的状态,会导致 `_contains3dTransform = true ` , 从而进入 `Element` 的逻辑。 + +```dart +Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: Container( + transform: Matrix4.identity()..setEntry(3, 2, 0.001) ..rotateX(100)..rotateY(100), + decoration: BoxDecoration( + color: Colors.red, + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 4.0, + offset: Offset(2, 2)) + ], + ), + child: Text( + "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + ), + ), + ), + ), +) +``` + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image29) + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image30) + + + +最后再来一个例子,这里回归到只有红色背景和阴影的情况,在之前它运行后是使用 `canvas` 标签来渲染文本,因为它的 `maskFilter != null`,但是这时候我们给 `Text` 配置上 `TextDecoratoin` ,运行之后可以看到背景颜色依然是 `canvas` ,但是文本又变成了 `p `标签的实现。 + +```dart + Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.red, + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 4.0, + offset: Offset(2, 2)) + ], + ), + child: Text( + "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + style: TextStyle(decoration: TextDecoration.lineThrough), + ), + ), + ), + ), + ); +``` + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image31) + + + +这是因为前面说过 `drawParagraph` , 在这个函数里有另外一个判断条件 `_drawOnCanvas ` , **在 Flutter Web 绘制文本时,当文本具备不为 `none` 的 `TextDecoration` 或者 `fontFeatures` 时,`_drawOnCanvas ` 就会被设置为 `fasle` ,从而变成使用 `p` 标签渲染的情况**。 + +> 这也很好理解,例如 **fontFeatures** 是影响字形选择的参数,如下图所示,这些行为在 Web 上用 Canvas 绘制相对会麻烦很多,关于 fontFeatures 可以参考 [《Flutter 上字体的另类玩法:FontFeature》](https://juejin.cn/post/7078680758826565662) + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image32) + + + +*那前面讲了那么多例子都是 `BitmapCanvas` , 那 `Domcanvas ` 什么时候会用到呢?* + +还记得前面列举的方法吗,需要进入 `_applyDomPaint ` 就需要 `hasArbitraryPaint == false` ,换言之就是没有文本,然后 `drawRect` 的时候没有 shader(` radient`) 等就可以了。 + +依然是前面的例子,绘制一个带有阴影的红色方框,但是此时把文本内容去掉,运行后可以看到不是 `canvas` 而是 `draw-rect` 标签,因为虽然此时 `maskFilter != null` (有 shadow),但是因为没有文本或者 shader(` gradient`) ,所以单纯普通的 `drawRect` 并不会触发 `hasArbitraryPaint == true`, 所以会直接使用 `Domcanvas` 绘制,完全脱离了 `canvas` 的渲染。 + +```dart +Scaffold( + body: Container( + alignment: Alignment.center, + child: Center( + child: Container( + height: 50, + decoration: BoxDecoration( + color: Colors.red, + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 4.0, + offset: Offset(2, 2)) + ], + ), + ), + ), + ), +) +``` + +![image-20220324172757163](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image33) + + + +所以最后总结一下:**首先除了下图所示之外的情况,大部分时候 Flutter Web 绘制都会进入到 `BitmapCanvas`**。 + + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image34) + +结合前面介绍的例子,进入到 `BitmapCanvas` 之后的流程可以总结: + +- 存在 ShaderMask 或者 ColorFilter 就会使用 Element ; +- 一般情况忽略 `_preserveImageData` ,有复杂矩阵变换时也是直接使用 Element ,因为复杂矩阵变换 canvas 支持并不好; +- _childOverdraw 经常和 _canvasPool.isEmpty 一起达成条件,一般有 picture 上有 _drawElement 之后就会调用 `_closeCurrentCanvas` 设置 `_childOverdraw = true ` 并且清空 _canvasPool; +- 结合上述第三个条件的状态,如果没有 maskFilter 或者 shader ,就会使用 Element 渲染 UI ; + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image35) + + + +最后针对文本,在 ` drawParagraph` 时还有特殊处理,关于 _childOverdraw 和 !isInsideSvgFilterTree 相关前面解释过了,新增条件是在有 TextDecoration 或者 FontFeatures 时,也会触发文本绘制变为 Element ,也就是 p + span 标签的形式。 + + + +![](http://img.cdn.guoshuyu.cn/20220627_Flutter-Web-T/image36) + + + +## 四、最后 + +虽然本次介绍的东西不少 ,但是 Flutter Web 在 html 渲染模式下的知识点远不止这些,而由小窥大,以 drawRect 和文本为切入点去了解 `SurfaceCanvas` 就是很不错的开始。 + +另外可以看到,在 Flutter Web 里有很多的自定义的 `` 标签,这些标签都是通过如 ` html.Element.tag('flt-canvas');` 等方式创建,它们和 Flutter 里的对应关系如下图片所示,如果感兴趣可以在 chrome 的 source 里对应的 `dart_sdk.js` 查看具体实现。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e997f415e144e6abc3e882639128841~tplv-k3u1fbpfcp-zoom-1.image \ No newline at end of file diff --git a/README.md b/README.md index b767c2a..d85e05e 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,13 @@ - [Flutter 2.5 发布啦,快来看看新特性](Flutter-250.md) - [Flutter 2.8 release 发布,快来看看新特性吧](Flutter-280.md) - [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md) + - [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md) - **Dart** - [Dart 2.12 发布,稳定空安全声明和FFI版本,Dart 未来的计划](Dart-212.md) - [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md) - [Dart 2.15 发布的新特性](Dart-215.md) - [Dart 2.16 发布的新特性](Dart-216.md) + - [Dart 2.17 发布的新特性](Dart-217.md) * [番外](FWREADME.md) @@ -137,7 +139,23 @@ * [Fluttter 混合开发下 HybridComposition 和 VirtualDisplay 的实现与未来演进](Flutter-HV.md) * [Flutter 双向聊天列表效果进阶优化](Flutter-Chat2.md) * [Flutter 上字体的另类玩法:FontFeature ](Flutter-FontFeature.md) + * [移动端系统生物认证技术详解](Flutter-BIO.md) + * [完整解析使用 Github Action 构建和发布 Flutter 应用](Flutter-GB.md) + * [Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结](Flutter-120HZ.md) + * [Flutter Festival | 2022 年 Flutter 适合我吗?Flutter VS Other 量化对比](Flutter-FF.md) + * [Flutter 从 TextField 安全泄漏问题深入探索文本输入流程](Flutter-TL.md) + * [Flutter iOS OC 混编 Swift 遭遇动态库和静态库问题填坑](Flutter-BIOS.md) * [Flutter Web : 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md) + * [大前端时代的乱流:带你了解最全面的 Flutter Web](Flutter-Web-T.md) + * [Flutter 深入探索混合开发的技术演进](Flutter-DWW.md) + * [Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer](Flutter-P3.md) + * [Google I/O Extended | Flutter 游戏和全平台正式版支持下 Flutter 的现状](Flutter-Extended.md) + * [掘金x得物公开课 - Flutter 3.0下的混合开发演进](Flutter-DWN.md) + * [Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty ](Flutter-N1.md) + * [Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3 ](Flutter-N2.md) + * [Flutter 小技巧之玩转字体渲染和问题修复 ](Flutter-N3.md) + * [Flutter 小技巧之有趣的动画技巧](Flutter-N4.md) + * [Flutter 小技巧之 Dart 里的 List 和 Iterable 你真的搞懂了吗?](Flutter-N6.md) diff --git a/SUMMARY.md b/SUMMARY.md index 03e0bba..ad8565e 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -57,11 +57,13 @@ - [Flutter 2.5 发布啦,快来看看新特性](Flutter-250.md) - [Flutter 2.8 release 发布,快来看看新特性吧](Flutter-280.md) - [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md) + - [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md) - **Dart** - [Dart 2.12 发布,稳定空安全声明和FFI版本,Dart 未来的计划](Dart-212.md) - [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md) - [Dart 2.15 发布的新特性](Dart-215.md) - [Dart 2.16 发布的新特性](Dart-216.md) + - [Dart 2.17 发布的新特性](Dart-217.md) * [番外](FWREADME.md) @@ -141,9 +143,43 @@ * [Flutter 上字体的另类玩法:FontFeature ](Flutter-FontFeature.md) + * [移动端系统生物认证技术详解](Flutter-BIO.md) + + * [完整解析使用 Github Action 构建和发布 Flutter 应用](Flutter-GB.md) + + * [Flutter 120hz 高刷新率在 Android 和 iOS 上的调研总结](Flutter-120HZ.md) + + * [Flutter Festival | 2022 年 Flutter 适合我吗?Flutter VS Other 量化对比](Flutter-FF.md) + + * [Flutter 从 TextField 安全泄漏问题深入探索文本输入流程](Flutter-TL.md) + + * [Flutter iOS OC 混编 Swift 遭遇动态库和静态库问题填坑](Flutter-BIOS.md) + * [Flutter Web : 一个编译问题带你了解 Flutter Web 的打包构建和分包实现 ](Flutter-WP.md) - + * [大前端时代的乱流:带你了解最全面的 Flutter Web](Flutter-Web-T.md) + + * [Flutter 深入探索混合开发的技术演进](Flutter-DWW.md) + + * [Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer](Flutter-P3.md) + + * [Google I/O Extended | Flutter 游戏和全平台正式版支持下 Flutter 的现状](Flutter-Extended.md) + + * [掘金x得物公开课 - Flutter 3.0下的混合开发演进](Flutter-DWN.md) + + * [Flutter 小技巧之 ButtonStyle 和 MaterialStateProperty ](Flutter-N1.md) + + * [Flutter 小技巧之 Flutter 3 下的 ThemeExtensions 和 Material3 ](Flutter-N2.md) + + * [Flutter 小技巧之玩转字体渲染和问题修复 ](Flutter-N3.md) + + * [Flutter 小技巧之有趣的动画技巧](Flutter-N4.md) + + * [Flutter 小技巧之 Dart 里的 List 和 Iterable 你真的搞懂了吗?](Flutter-N6.md) + + + + diff --git a/UPDATE.md b/UPDATE.md index 1d4038e..ae38141 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -10,6 +10,7 @@ - [Flutter 2.5 发布啦,快来看看新特性](Flutter-250.md) - [Flutter 2.8 release 发布,快来看看新特性吧](Flutter-280.md) - [Flutter 2.10 release 发布,快来看看新特性吧](Flutter-2100.md) +- [Flutter 3.0 发布啦~快来看看有什么新功能-2022 Google I/O](Flutter-300.md) @@ -21,4 +22,5 @@ - [Dart 2.14 发布,新增语言特性和共享标准 lint](Dart-214.md) - [Dart 2.15 发布的新特性](Dart-215.md) - [Dart 2.16 发布的新特性](Dart-216.md) +- [Dart 2.16 发布的新特性](Dart-217.md)