快速并发、构造函数拆分、改进的枚举等
原文作者:medium.com/@mit.mit
发布时间:2021年12月9日 - 9分钟阅读
今天我们发布了2.15版本的Dart SDK,其特点是使用worker isolates的快速并发,新的构造函数拆分语言功能,改进dart:core库的枚举支持,包发布者的新功能,以及更多。
使用工作者隔离器的快速并发性
几乎所有的现代设备都有多核的CPU,能够并行地运行多个任务。对于大多数Dart程序来说,这些内核的使用对于作为开发者的你来说是透明的:Dart运行系统默认在一个内核上运行你的所有Dart代码,但然后使用额外的内核来执行系统级任务,如同步输入/输出,如写文件或进行网络调用。
但你的Dart代码本身可能需要并发运行。例如,你可能有一个连续的动画和一个长期运行的任务,如解析一个大型JSON文件。如果额外的任务耗时过长,可能会导致用户界面的卡顿或滞后。通过将这些额外的任务转移到一个单独的核心,动画可以继续在执行的主线程上运行,不受干扰。
Dart的并发模型是基于隔离物--相互隔离的独立执行单元--来防止一大类与共享内存有关的并发编程错误,如数据竞赛等竞赛条件。Dart通过不允许任何可变对象在隔离区之间共享来防止这些错误,而是使用一个隔离区使用消息传递交换状态的模型。在Dart 2.15中,我们对隔离体进行了大量的改进。
我们首先重新设计和实现了隔离区的工作方式,引入了一个新的概念:隔离区组。隔离组中的隔离体共享代表运行程序的各种内部数据结构。这使得组中的单个隔离器的成本大大降低。现在,在一个现有的隔离组中启动一个额外的隔离体要快100多倍,因为我们不需要初始化程序结构,而且这些生成的隔离体消耗的内存要少10-100倍。
虽然隔离组仍然阻止了隔离体之间对可变对象的共享访问,但隔离组是通过共享堆来实现的,这就解锁了更多的功能。我们可以将对象从一个隔离体传递到另一个隔离体,这可以用于执行返回大片内存的任务的工作隔离体。一个例子是一个工作隔离区,它通过网络调用获取数据,将数据解析成一个大的JSON对象图,然后将该JSON图返回给主隔离区。在Dart 2.15之前,需要对该结果进行深度复制,如果复制的时间超过了帧的预算,这本身就会导致UI卡顿。
在2.15版本中,工作隔离体可以调用Isolate.exit(),并将其结果作为一个参数传递。然后,Dart运行时将包含结果的内存从工作者隔离区传递给主隔离区,而无需复制,主隔离区可以在恒定时间内接收结果。我们在Flutter 2.8中更新了compute()实用函数,以利用Isolate.exit()的优势。如果您已经在使用compute(),那么升级到Flutter 2.8后,您将自动获得这些性能提升。
最后,我们重新设计了Isolate消息传递机制的实现方式,使中小尺寸消息的传递速度提高了约8倍。发送速度明显加快,而接收消息几乎总是在恒定的时间内完成。我们还扩展了隔离器之间可以相互发送的对象的种类,增加了对函数类型、闭包和堆栈跟踪对象的支持。详情请见SendPort.send()的API文档。
要了解更多关于如何使用隔离器的信息,请看我们为2.15版新增的Dart并发性文档。我们还提供了一些代码样本,你可以查看一下。
新的语言特性。构造函数拆分
在Dart中,你可以通过使用函数的名称来创建一个函数对象,它指向另一个对象上的函数。在下面的例子中,main()方法的第二行说明了这种语法,它将g设置为m.greet。
class Greeter {
final String name;
Greeter(this.name);
void greet(String who) {
print('$name says: Hello $who!');
}
}
void main() {
final m = Greeter('Michael');
final g = m.greet; // g holds a function pointer to m.greet.
g('Leaf'); // Invokes and prints "Michael says: Hello Leaf!"
}
这种函数指针--也被称为函数拆分--在使用Dart核心库时经常出现。下面是一个通过传递函数指针在一个迭代器上调用foreach()的例子。
final m = Greeter('Michael');
['Lasse', 'Bob', 'Erik'].forEach(m.greet);
// Prints "Michael says: Hello Lasse!", "Michael says: Hello Bob!",
// "Michael says: Hello Erik!"
一直以来,我们都不支持从构造函数中创建拆分(语言问题#216)。这很烦人,因为在很多情况下--例如,在构建Flutter UIs时--构造函数拆分是你需要的。从Dart 2.15开始,这种语法现在得到了支持。这里有一个例子,通过调用.map()并将其传递给Text的构造函数,建立一个包含三个Text小部件的Column小部件。
class FruitWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: ['Apple', 'Orange'].map(Text.new).toList());
}
}
Text.new指的是Text类的默认构造函数。你也可以引用一个命名的构造函数--例如,.map(Text.rich)。
相关的语言变化
在我们实现构造函数拆分的同时,我们利用这个机会修复了我们现有的对函数指针的支持中的一些不一致之处。现在你可以将一个泛型方法特殊化,以创建一个非泛型方法。
T id<T>(T value) => value;
var intId = id<int>; // New in 2.15.
int Function(int) intId = id; // Pre-2.15 workaround.
你甚至可以将一个通用函数对象特殊化,以创建一个非通用的函数对象。
const fo = id; // Tear off `id`, creating a function object.
const c1 = fo<int>; // New in 2.15; error before.
最后,我们清理了涉及泛型的类型字面。
var y = List; // Already supported.
var z = List<int>; // New in 2.15.
var z = typeOf<List<int>>(); // Pre-2.15 workaround.
在dart:core库中改进了枚举功能
我们对dart:core库中的枚举API进行了一些方便的补充(语言问题#1511)。现在你可以使用.name获得每个枚举值的String值。
enum MyEnum {
one, two, three
}
void main() {
print(MyEnum.one.name); // Prints "one".
}
你也可以通过名字查询一个枚举值。
print(MyEnum.values.byName('two') == MyEnum.two); // Prints "true".
最后,你可以得到一个所有名-值对的地图。
final map = MyEnum.values.asNameMap();
print(map['three'] == MyEnum.three); // Prints "true".
关于使用这些新API的例子,请看这个Flutter PR。
压缩的指针
Dart 2.15增加了对压缩指针的支持,这是一种技术,如果只需要支持32位的地址空间(最多4GB的内存),64位的SDK可以使用一个更节省空间的指针表示。压缩指针导致内存大幅减少;在我们对GPay应用的内部测试中,我们看到Dart堆的大小减少了约10%。
由于压缩指针意味着不能解决任何超过4GB的可用内存,该功能是Dart SDK中的一个配置选项,只能由Dart SDK的嵌入者在构建SDK时进行切换。Flutter SDK 2.8版已经为Android构建启用了这一配置,Flutter团队正在考虑在未来的版本中也为iOS构建启用这一配置。
Dart SDK中包含的Dart DevTools
调试和性能工具的DevTools套件以前并不在Dart SDK中;你必须单独下载它。从Dart 2.15开始,你在下载Dart SDK时就可以得到DevTools,不需要进一步的安装步骤。关于在Dart命令行应用程序中使用DevTools的更多信息,请参阅DevTools文档。
为软件包发布者提供新的pub功能
Dart 2.15 SDK在dart pub开发者命令和pub.dev软件包库中也有两个新功能。
首先,有一个针对软件包发布者的新安全功能。其目的是检测发布者何时意外地在pub包内发布了秘密--例如云或CI凭证。在了解到GitHub仓库内每天有成千上万的秘密被泄露后,我们受到启发,增加了这种泄露检测。
泄漏检测作为dart pub publish命令中发布前验证的一部分运行。如果它检测到即将发布的文件中存在潜在的秘密,发布命令就会退出而不发布,并打印出这样的输出。
Publishing my_package 1.0.0 to https://pub.dartlang.org:
Package validation found the following errors:
* line 1, column 1 of lib/key.pem: Potential leak of Private Key detected.
╷
1 │ ┌ - - -BEGIN PRIVATE KEY - - -
2 │ │ H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
3 │ └ - - -END PRIVATE KEY - - -
╵
* line 2, column 23 of lib/my_package.dart: Potential leak of Google OAuth Refresh Token detected.
╷
2 │ final refreshToken = "1//042ys8uoFwZrkCgYIARAAGAQSNwF-L9IrXmFYE-sfKefSpoCnyqEcsHX97Y90KY-p8TPYPPnY2IPgRXdy0QeVw7URuF5u9oUeIF0";
在少数情况下,这种检测可能会出现假阳性,标记出你确实打算发布的内容或文件的潜在泄漏。在这些情况下,你可以将这些文件添加到允许列表中。
第二,我们为发布者增加了另一项功能,支持撤回已经发布的软件包版本。当一个有问题的软件包版本被发布时,我们通常建议发布一个新的版本,用一个小的增量来修复这个意外的问题。在极少数情况下--例如,当你还没有这样的修复方法,或者你不小心发布了一个新的大版本,但打算发布一个新的小版本--你可以使用新的软件包撤回功能作为最后手段。这个功能可以在pub.dev的管理界面上使用。
当一个软件包的版本被收回时,pub客户端在pub get或pub upgrade中不再解析到该版本。如果有任何开发人员已经解析到被撤回的版本(并且因此在他们的pubspec.lock文件中),他们在下次运行pub时将看到一个警告。
$ dart pub get
Resolving dependencies…
mypkg 0.0.181-buggy (retracted, 0.0.182-fixed available)
Got dependencies!
检测双向Unicode字符的安全分析(CVE-2021-22567)
最近发现了一个涉及双向Unicode字符的一般编程语言漏洞(CVE-2021-42574)。这个漏洞影响到大多数支持Unicode的现代编程语言。下面的Dart源代码说明了这个问题。
main() {
final accessLevel = 'user';
if (accessLevel == 'user .// Check if admin ') {
print('You are a regular user.');
} else {
print('You are an admin.');
}
}
你可能会认为这个程序打印的是 "你是普通用户",但实际上它可能打印的是 "你是管理员"! 通过使用一个包含双向Unicode字符的字符串,这个漏洞是可能的。这些字符可以改变文本的方向,从左到右,从右到左,再从左到右,都在一行之内。有了双向字符,文本在屏幕上的呈现就会与实际的文本内容大不相同。你可以在GitHub的代码gist中看到这方面的例子。
针对这个漏洞的缓解措施包括使用能够检测双向Unicode字符的工具(编辑器、代码审查工具等),这样就可以让开发者意识到它们,并在知情的情况下接受它们的使用。上面链接的GitHub gist文件查看器是揭示这些字符的工具的一个例子。
Dart 2.15引入了一个进一步的缓解措施(Dart安全公告CVE-2021-22567):Dart分析器现在扫描双向Unicode字符,并标记任何对它们的使用。
$ dart analyze
Analyzing cvetest... 2.6s
info • bin/cvetest.dart:4:27 • The Unicode code point 'U+202E'
changes the appearance of text from how it's interpreted
by the compiler. Try removing the code point or using the
Unicode escape sequence '\u202E'. •
text_direction_code_point_in_literal
我们建议用Unicode转义序列替换这些字符,这样它们在任何文本编辑器或查看器中都是可见的。另外,如果你对这些字符有合法的使用,你可以通过在使用前的一行中添加一个覆盖来禁用警告。
// ignore: text_direction_code_point_in_literal
使用第三方pub服务器时的Pub.dev凭证漏洞(CVE-2021-22568)
我们还将发布第二个与pub.dev相关的Dart安全公告。CVE-2021-22568。该公告针对的是那些可能向第三方pub软件包服务器(如私人或公司内部的软件包服务器)发布软件包的软件包发布者。只发布到公共pub.dev仓库(标准配置)的开发者不受这个漏洞的影响。
如果你已经发布到了第三方软件库,那么这个漏洞就是在第三方软件库进行认证时出示的OAuth2临时(一小时)访问令牌可以被滥用来对公共pub.dev软件库进行认证。因此,一个恶意的第三方pub服务器可能会使用访问令牌来冒充你在pub.dev上发布软件包。如果你已经发布了一个软件包到一个不受信任的第三方软件包仓库,考虑对你在pub.dev公共软件包仓库的所有账户活动进行审计。你可以使用 pub.dev 的活动日志来达到这个目的。
结束语
我们希望你会喜欢今天发布的Dart 2.15的新功能。这是我们今年的最后一个版本,我们想借此机会表达我们对美妙的Dart生态系统的感激之情。感谢所有伟大的反馈,感谢你们对我们持续增长的支持,感谢你们在过去一年中通过在pub.dev上发布的数千个包来扩展我们的生态系统。我们已经迫不及待地想在明年重新开始,而且我们有很多令人兴奋的事情计划在2022年进行。在那之前,请享受假期!