序言
2021年的最后一天, Dart 官方發(fā)布了 dart 2.15 版本,該版本優(yōu)化了很多內(nèi)容,今天我們要重點(diǎn)說說 isolate 工作器。官方推文鏈接
在探索新變化之前,我們來回憶鞏固一下 isolate 的使用。
isolate 的作用
問題:Flutter 基于單線程模式使用協(xié)程進(jìn)行開發(fā),為什么還需要 isolate ?
首先我們要明確 并行(isolate) 與并發(fā)(future)的區(qū)別。下面我們通過簡單的例子來進(jìn)行說明 。Demo 是一個(gè)簡單的頁面,中間放置一個(gè)不斷轉(zhuǎn)圈的 progress 和一個(gè)按鍵,按鍵用來觸發(fā)耗時(shí)方法。
///計(jì)算偶數(shù)個(gè)數(shù)(具體的耗時(shí)操作)下面示例代碼中會用到
static int calculateEvenCount(int num) {
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
}
return count;
}
///按鍵點(diǎn)擊事件
onPressed: () {
//觸發(fā)耗時(shí)操作
doMockTimeConsume();
}
- 方式一: 我們將耗時(shí)操作使用
future的方式進(jìn)行封裝
///使用future的方式封裝耗時(shí)操作
static Future<int> futureCountEven(int num) async {
var result = calculateEvenCount(num);
return Future.value(result);
}
///耗時(shí)事件
void doMockTimeConsume() async {
var result = await futureCountEven(1000000000);
_count = result;
setState(() {});
}
結(jié)果如下:

結(jié)論:使用
future 的方式來消費(fèi)耗時(shí)操作,由于仍然是單線程在進(jìn)行工作,異步只是在同一個(gè)線程的并發(fā)操作,仍會阻塞UI的刷新。
- 方式二: 使用
isolate開辟新線程,避開主線程,不干擾UI刷新
//模擬耗時(shí)操作
void doMockTimeConsume() async {
var result = await isolateCountEven(1000000000);
_count = result;
setState(() {});
}
///使用isolate的方式封裝耗時(shí)操作
static Future<dynamic> isolateCountEven(int num) async {
final p = ReceivePort();
///發(fā)送參數(shù)
await Isolate.spawn(_entryPoint, [p.sendPort, num]);
return (await p.first) as int;
}
static void _entryPoint(List<dynamic> args) {
SendPort responsePort = args[0];
int num = args[1];
///接收參數(shù),進(jìn)行耗時(shí)操作后返回?cái)?shù)據(jù)
responsePort.send(calculateEvenCount(num));
}
結(jié)果如下:

結(jié)論:使用
isolate 實(shí)現(xiàn)了多線程并行,在新線程中進(jìn)行耗時(shí)操作不會干擾UI線程的刷新。
isolate 的局限性,為什么需要優(yōu)化?
iso 有兩點(diǎn)較為重要的局限性。
- isolate 消耗較重,除了創(chuàng)建耗時(shí),每次創(chuàng)建還至少需要2Mb的空間,有OOM的風(fēng)險(xiǎn)。
- isolate 之間的內(nèi)存空間各自獨(dú)立,當(dāng)參數(shù)或結(jié)果跨 iso 相互傳遞時(shí)需要深度拷貝,拷貝耗時(shí),可能造成UI卡頓。
isolate 新特性
Dart 2.15 更新, 給 iso 添加了組的概念,isolate 組 工作特征可簡單總結(jié)為以下兩點(diǎn):
- Isolate 組中的 isolate 共享各種內(nèi)部數(shù)據(jù)結(jié)構(gòu)
- Isolate 組
仍然阻止在 isolate 間共享訪問可變對象,但由于 isolate 組使用共享堆實(shí)現(xiàn),這也讓其擁有了更多的功能。
官方推文中舉了一個(gè)例子:
工作器 isolate 通過網(wǎng)絡(luò)調(diào)用獲得數(shù)據(jù),將該數(shù)據(jù)解析為大型 JSON 對象圖,然后將這個(gè) JSON 圖返回到主 isolate 中。
Dart 2.15 之前:執(zhí)行該操作需要深度復(fù)制,如果復(fù)制花費(fèi)的時(shí)間超過幀預(yù)算時(shí)間,就會導(dǎo)致界面卡頓。
使用 Dart 2.15:工作器 isolate 可以調(diào)用Isolate.exit(),將其結(jié)果作為參數(shù)傳遞。然后,Dart 運(yùn)行時(shí)將包含結(jié)果的內(nèi)存數(shù)據(jù)從工作器 isolate 傳遞到主 isolate 中,無需復(fù)制,且主 isolate 可以在固定時(shí)間內(nèi)接收結(jié)果。
重點(diǎn):提供 Isolate.exit() 方法,將包含結(jié)果的內(nèi)存數(shù)據(jù)從工作器 isolate 傳遞到主 isolate ,過程無需復(fù)制。
附注: 使用 Dart 新特性,需將 flutter sdk 升級到 2.8.0 以上 鏈接。
exit 和 send 的區(qū)別及用法
Dart 更新后,我們將數(shù)據(jù)從 工作器 isolate(子線程)回傳到 主 isolate(主線程)有兩種方式。
- 方式一: 使用
send
responsePort.send(data);
點(diǎn)擊進(jìn)入 send 方法查看源碼注釋,看到這樣一句話:

結(jié)論:send 本身不會阻塞,會立即發(fā)送,但可能需要線性時(shí)間成本用于復(fù)制數(shù)據(jù)。
- 方式二:使用
exit
Isolate.exit(responsePort, data);
官網(wǎng) 給出的解釋如下:

結(jié)論:隔離之間的消息傳遞通常涉及數(shù)據(jù)復(fù)制,因此可能會很慢,并且會隨著消息大小的增加而增加。但是
exit(),則是在退出隔離中保存消息的內(nèi)存,不會被復(fù)制,而是被傳輸?shù)街?isolate。這種傳輸很快,并且在恒定的時(shí)間內(nèi)完成。
我們把上面 demo 中的 _entryPoint 方法做一下優(yōu)化修改:
static void _entryPoint(SendPort port) {
SendPort responsePort = args[0];
int num = args[1];
///接收參數(shù),進(jìn)行耗時(shí)操作后返回?cái)?shù)據(jù)
//responsePort.send(calculateEvenCount(num));
Isolate.exit(responsePort, calculateEvenCount(num));
}
總結(jié):使用 exit() 替代 SendPort.send,可規(guī)避數(shù)據(jù)復(fù)制,節(jié)省耗時(shí)。
isolate 組
如何創(chuàng)建一個(gè) isolate 組?官方給出的解釋如下:
When an isolate calls
Isolate.spawn(), the two isolates have the same executable code and are in the same isolate group. Isolate groups enable performance optimizations such as sharing code; a new isolate immediately runs the code owned by the isolate group. Also,Isolate.exit()works only when the isolates are in the same isolate group.
當(dāng)在 isolate 中調(diào)用另一個(gè) isolate 時(shí),這兩個(gè) isolate 具有相同的可執(zhí)行代碼,并且位于同一隔離組。
PS: 小轟暫時(shí)也沒有想到具體的使用場景,先暫放一邊吧。
實(shí)踐:isolate 如何處理連續(xù)數(shù)據(jù)
結(jié)合上面的耗時(shí)方法calculateEvenCount,isolate 處理連續(xù)數(shù)據(jù)需要結(jié)合 stream 流 的設(shè)計(jì)。具體 demo 如下:
///測試入口
static testContinuityIso() async {
final numbs = [10000, 20000, 30000, 40000];
await for (final data in _sendAndReceive(numbs)) {
log(data.toString());
}
}
///具體的iso實(shí)現(xiàn)(主線程)
static Stream<Map<String, dynamic>> _sendAndReceive(List<int> numbs) async* {
final p = ReceivePort();
await Isolate.spawn(_entry, p.sendPort);
final events = StreamQueue<dynamic>(p);
// 拿到 子isolate傳遞過來的 SendPort 用于發(fā)送數(shù)據(jù)
SendPort sendPort = await events.next;
for (var num in numbs) {
//發(fā)送一條數(shù)據(jù),等待一條數(shù)據(jù)結(jié)果,往復(fù)循環(huán)
sendPort.send(num);
Map<String, dynamic> message = await events.next;
//每次的結(jié)果通過stream流外露
yield message;
}
//發(fā)送 null 作為結(jié)束標(biāo)識符
sendPort.send(null);
await events.cancel();
}
///具體的iso實(shí)現(xiàn)(子線程)
static Future<void> _entry(SendPort p) async {
final commandPort = ReceivePort();
//發(fā)送一個(gè) sendPort 給主iso ,用于 主iso 發(fā)送參數(shù)給 子iso
p.send(commandPort.sendPort);
await for (final message in commandPort) {
if (message is int) {
final data = calculateEvenCount(message);
p.send(data);
} else if (message == null) {
break;
}
}
}
拋磚引玉,這只是一個(gè)思路~