一 . 原始代碼
為什么要Isolate,我們先看一段比較簡(jiǎn)單的代碼:
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
class TestWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return TestWidgetState();
}
}
class TestWidgetState extends State<TestWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
children: <Widget>[
Container(
width: 100,
height: 100,
child: CircularProgressIndicator(),
),
FlatButton(
onPressed: () async {
_count = countEven(1000000000);
setState(() {});
},
child: Text(
_count.toString(),
)),
],
mainAxisSize: MainAxisSize.min,
),
),
);
}
//計(jì)算偶數(shù)的個(gè)數(shù)
static int countEven(int num) {
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
}
return count;
}
}
UI包含兩個(gè)部分,一個(gè)不斷轉(zhuǎn)圈的progress指示器,一個(gè)按鈕,當(dāng)點(diǎn)擊按鈕的時(shí)候,找出比某個(gè)正整數(shù)n小的數(shù)的偶數(shù)的個(gè)數(shù)(請(qǐng)忽視具體算法,故意做耗時(shí)計(jì)算用,哈哈)。我們來(lái)運(yùn)行一下代碼看看效果:
可以看到,本來(lái)是很流暢的轉(zhuǎn)圈,當(dāng)我點(diǎn)擊按鈕計(jì)算的時(shí)候,UI出現(xiàn)了卡頓,為什么會(huì)出現(xiàn)卡頓,因?yàn)槲覀兊挠?jì)算默認(rèn)是在UI線程中的,當(dāng)我們調(diào)用countEven的時(shí)候,這個(gè)計(jì)算需要耗時(shí),而在這期間,UI是沒(méi)有機(jī)會(huì)去調(diào)用刷新的,因此會(huì)卡頓,計(jì)算完成后,UI恢復(fù)正常刷新。
二. 使用async優(yōu)化
那么有些同學(xué)就會(huì)說(shuō)了,在dart中,有async關(guān)鍵字,我們可以用異步計(jì)算,這樣就不會(huì)影響UI的刷新了,事實(shí)真的是這樣嗎?我們一起來(lái)修改一下代碼:
a. 將count改為asyncCountEven
static Future<int> asyncCountEven(int num) async{
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
}
return count;
}
b. 調(diào)用:
_count = await asyncCountEven(1000000000);
我們繼續(xù)運(yùn)行一下代碼,看現(xiàn)象:
仍然卡頓,說(shuō)明異步是解決不了問(wèn)題的,為什么?因?yàn)槲覀內(nèi)耘f是在同一個(gè)UI線程中做運(yùn)算,異步只是說(shuō)我可以先運(yùn)行其他的,等我這邊有結(jié)果再返回,但是,記住,我們的計(jì)算仍舊是在這個(gè)UI線程,仍會(huì)阻塞UI的刷新,異步只是在同一個(gè)線程的并發(fā)操作。
三. 使用compute優(yōu)化
那么我們?cè)趺唇鉀Q這個(gè)問(wèn)題呢,其實(shí)很簡(jiǎn)單,我們知道卡頓的原因是在同一個(gè)線程中導(dǎo)致的,那我們有沒(méi)有辦法將計(jì)算移到新的線程中呢,當(dāng)然是可以的。不過(guò)在dart中,這里不是稱(chēng)呼線程,是Isolate,直譯叫做隔離,這么古怪的名字,是因?yàn)楦綦x不共享數(shù)據(jù),每個(gè)隔離中的變量都是不同的,不能相互共享。
但是由于dart中的Isolate比較重量級(jí),UI線程和Isolate中的數(shù)據(jù)的傳輸比較復(fù)雜,因此flutter為了簡(jiǎn)化用戶(hù)代碼,在foundation庫(kù)中封裝了一個(gè)輕量級(jí)compute操作,我們先看看compute,然后再來(lái)看Isolate。
要使用compute,必須注意的有兩點(diǎn),一是我們的compute中運(yùn)行的函數(shù),必須是頂級(jí)函數(shù)或者是static函數(shù),二是compute傳參,只能傳遞一個(gè)參數(shù),返回值也只有一個(gè),我們先看看本例中的compute優(yōu)化吧:
真的很簡(jiǎn)單,只用在使用的時(shí)候,放到compute函數(shù)中就行了。
_count = await compute(countEven, 1000000000);
再次運(yùn)行,我們來(lái)看看效果吧:
可以看到,現(xiàn)在的計(jì)算并不會(huì)導(dǎo)致UI卡頓,完美解決問(wèn)題。
四. 使用Isolate優(yōu)化
但是,compute的使用還是有些限制,它沒(méi)有辦法多次返回結(jié)果,也沒(méi)有辦法持續(xù)性的傳值計(jì)算,每次調(diào)用,相當(dāng)于新建一個(gè)隔離,如果調(diào)用過(guò)多的話反而會(huì)適得其反。在某些業(yè)務(wù)下,我們可以使用compute,但是在另外一些業(yè)務(wù)下,我們只能使用dart提供的Isolate了,我們先看看Isolate在本例中的使用:
a. 增加這兩個(gè)函數(shù)
static Future<dynamic> isolateCountEven(int num) async {
final response = ReceivePort();
await Isolate.spawn(countEvent2, response.sendPort);
final sendPort = await response.first;
final answer = ReceivePort();
sendPort.send([answer.sendPort, num]);
return answer.first;
}
static void countEvent2(SendPort port) {
final rPort = ReceivePort();
port.send(rPort.sendPort);
rPort.listen((message) {
final send = message[0] as SendPort;
final n = message[1] as int;
send.send(countEven(n));
});
}
b. 使用
_count = await isolateCountEven(1000000000);
相對(duì)于compute復(fù)雜了很多,效果就不貼了,和compute一樣,毫無(wú)卡頓。。
代價(jià)是什么
對(duì)于我們來(lái)說(shuō),其實(shí)是把多線程當(dāng)做一種計(jì)算資源來(lái)使用的。我們可以通過(guò)創(chuàng)建新的 isolate 計(jì)算 heavy work,從而減輕 UI 線程的負(fù)擔(dān)。但是這樣做的代價(jià)是什么呢?
時(shí)間
通常來(lái)說(shuō),當(dāng)我們使用多線程計(jì)算的時(shí)候,整個(gè)計(jì)算的時(shí)間會(huì)比單線程要多,額外的耗時(shí)是什么呢?
- 創(chuàng)建 Isolate
- Copy Message
當(dāng)我們按照上面的代碼執(zhí)行一段多線程代碼時(shí),經(jīng)歷了 isolate 的創(chuàng)建以及銷(xiāo)毀過(guò)程。下面是一種我們?cè)诮馕?json 中這樣編寫(xiě)代碼可能的方式。
static BSModel toBSModel(String json){}
parsingModelList(List<String> jsonList) async{
for(var model in jsonList){
BSModel m = await compute(toBSModel, model);
}
}
復(fù)制代碼
在解析 json 的時(shí)候,我們可能通過(guò) compute 把解析任務(wù)放在新的 isolate 中完成,然后把值傳過(guò)來(lái)。這時(shí)候我們會(huì)發(fā)現(xiàn),整個(gè)解析會(huì)變得異常的慢。這是由于我們每次創(chuàng)建 BSModel 的時(shí)候都經(jīng)歷了一次 isolate 的創(chuàng)建以及銷(xiāo)毀過(guò)程。這將會(huì)耗費(fèi)約 50-150ms 的時(shí)間。
在這之中,我們傳遞 data 也經(jīng)歷了 Network -> Main Isolate -> New Isolate (result) -> Main Isolate,多出來(lái)兩次 copy 的操作。如果我們是在 Main 線程之外的 isolate 下載的數(shù)據(jù),那么就可以直接在該線程進(jìn)行解析,最后只需要傳回 Main Isolate 即可,省下了一次 copy 操作。(Network -> New Isolate (result)-> Main Isolate)
空間
Isolate 實(shí)際上是比較重的,每當(dāng)我們創(chuàng)建出來(lái)一個(gè)新的 Isolate 至少需要 2mb 左右的空間甚至更多,取決于我們具體 isolate 的用途。
OOM 風(fēng)險(xiǎn)
我們可能會(huì)使用 message 傳遞 data 或 file。而實(shí)際上我們傳遞的 message 是經(jīng)歷了一次 copy 過(guò)程的,這其實(shí)就可能存在著 OOM 的風(fēng)險(xiǎn)。
如果說(shuō)我們想要返回一個(gè) 2GB 的 data,在 iPhone X(3GB ram)上,我們是無(wú)法完成 message 的傳遞操作的。
Tips
上面已經(jīng)介紹了使用 isolate 進(jìn)行多線程操作會(huì)有一些額外的 cost,那么是否可以通過(guò)一些手段減少這些消耗呢。我個(gè)人建議從兩個(gè)方向上入手。
- 減少 isolate 創(chuàng)建所帶來(lái)的消耗。
- 減少 message copy 次數(shù),以及大小。
使用 LoadBalancer
如何減少 isolate 創(chuàng)建所帶來(lái)的消耗呢。自然一個(gè)想法就是能否創(chuàng)建一個(gè)線程池,初始化到那里。當(dāng)我們需要使用的時(shí)候再拿來(lái)用就好了。
實(shí)際上 dart team 已經(jīng)為我們寫(xiě)好一個(gè)非常實(shí)用的 package,其中就包括 LoadBalancer。
我們現(xiàn)在 pubspec.yaml 中添加 isolate 的依賴(lài)。
isolate: ^2.0.2
復(fù)制代碼
然后我們可以通過(guò) LoadBalancer 創(chuàng)建出指定個(gè)數(shù)的 isolate。
Future<LoadBalancer> loadBalancer = LoadBalancer.create(2, IsolateRunner.spawn);
復(fù)制代碼
這段代碼將會(huì)創(chuàng)建出一個(gè) isolate 線程池,并自動(dòng)實(shí)現(xiàn)了負(fù)載均衡。
由于 dart 天生支持頂層函數(shù),我們可以在 dart 文件中直接創(chuàng)建這個(gè) LoadBalancer。下面我們?cè)賮?lái)看看應(yīng)該如何使用 LoadBalancer 中的 isolate。
int useLoadBalancer() async {
final lb = await loadBalancer;
int res = await lb.run<int, int>(_doSomething, 1);
return res;
}
復(fù)制代碼
我們關(guān)注的只有 Future<R> run<R, P>(FutureOr<R> function(P argument), argument, 方法。我們還是需要傳入一個(gè) function 在某個(gè) isolate 中運(yùn)行,并傳入其參數(shù) argument。run 方法將會(huì)返回我們執(zhí)行方法的返回值。
整體和 compute 使用感覺(jué)上差不多,但是當(dāng)我們多次使用額外的 isolate 的時(shí)候,不再需要重復(fù)創(chuàng)建了。
并且 LoadBalancer 還支持 runMultiple,可以讓一個(gè)方法在多線程中執(zhí)行。具體使用請(qǐng)查看 api。
LoadBalancer 經(jīng)過(guò)測(cè)試,它會(huì)在第一次使用其 isolate 的時(shí)候初始化線程池。

當(dāng)應(yīng)用打開(kāi)后,即使我們?cè)陧攲雍瘮?shù)中調(diào)用了 LoadBalancer.create,但是還是只會(huì)有一個(gè) Isolate。

當(dāng)我們調(diào)用 run 方法時(shí),才真正創(chuàng)建出了實(shí)際的 isolate。