一 . 原始代碼
為什么要Isolate,我們先看一段比較簡單的代碼:
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ù)(請忽視具體算法道媚,故意做耗時(shí)計(jì)算用,哈哈)欢嘿。我們來運(yùn)行一下代碼看看效果:
可以看到衰琐,本來是很流暢的轉(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是沒有機(jī)會(huì)去調(diào)用刷新的匿刮,因此會(huì)卡頓,計(jì)算完成后探颈,UI恢復(fù)正常刷新熟丸。
二. 使用async優(yōu)化
那么有些同學(xué)就會(huì)說了,在dart中伪节,有async關(guān)鍵字光羞,我們可以用異步計(jì)算,這樣就不會(huì)影響UI的刷新了怀大,事實(shí)真的是這樣嗎纱兑?我們一起來修改一下代碼:
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)象:
仍然卡頓化借,說明異步是解決不了問題的潜慎,為什么?因?yàn)槲覀內(nèi)耘f是在同一個(gè)UI線程中做運(yùn)算,異步只是說我可以先運(yùn)行其他的铐炫,等我這邊有結(jié)果再返回垒手,但是,記住驳遵,我們的計(jì)算仍舊是在這個(gè)UI線程淫奔,仍會(huì)阻塞UI的刷新,異步只是在同一個(gè)線程的并發(fā)操作堤结。
三. 使用compute優(yōu)化
那么我們怎么解決這個(gè)問題呢,其實(shí)很簡單鸭丛,我們知道卡頓的原因是在同一個(gè)線程中導(dǎo)致的竞穷,那我們有沒有辦法將計(jì)算移到新的線程中呢,當(dāng)然是可以的鳞溉。不過在dart中瘾带,這里不是稱呼線程,是Isolate熟菲,直譯叫做隔離看政,這么古怪的名字,是因?yàn)楦綦x不共享數(shù)據(jù)抄罕,每個(gè)隔離中的變量都是不同的允蚣,不能相互共享。
但是由于dart中的Isolate比較重量級呆贿,UI線程和Isolate中的數(shù)據(jù)的傳輸比較復(fù)雜嚷兔,因此flutter為了簡化用戶代碼,在foundation庫中封裝了一個(gè)輕量級compute操作做入,我們先看看compute冒晰,然后再來看Isolate。
要使用compute竟块,必須注意的有兩點(diǎn)壶运,一是我們的compute中運(yùn)行的函數(shù),必須是頂級函數(shù)或者是static函數(shù)浪秘,二是compute傳參蒋情,只能傳遞一個(gè)參數(shù),返回值也只有一個(gè)秫逝,我們先看看本例中的compute優(yōu)化吧:
真的很簡單恕出,只用在使用的時(shí)候,放到compute函數(shù)中就行了违帆。
_count = await compute(countEven, 1000000000);
再次運(yùn)行浙巫,我們來看看效果吧:
可以看到,現(xiàn)在的計(jì)算并不會(huì)導(dǎo)致UI卡頓,完美解決問題的畴。
四. 使用Isolate優(yōu)化
但是渊抄,compute的使用還是有些限制,它沒有辦法多次返回結(jié)果丧裁,也沒有辦法持續(xù)性的傳值計(jì)算护桦,每次調(diào)用,相當(dāng)于新建一個(gè)隔離煎娇,如果調(diào)用過多的話反而會(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);
相對于compute復(fù)雜了很多哟绊,效果就不貼了因妙,和compute一樣,毫無卡頓票髓。攀涵。
代價(jià)是什么
對于我們來說,其實(shí)是把多線程當(dāng)做一種計(jì)算資源來使用的洽沟。我們可以通過創(chuàng)建新的 isolate 計(jì)算 heavy work以故,從而減輕 UI 線程的負(fù)擔(dān)。但是這樣做的代價(jià)是什么呢玲躯?
時(shí)間
通常來說据德,當(dāng)我們使用多線程計(jì)算的時(shí)候,整個(gè)計(jì)算的時(shí)間會(huì)比單線程要多跷车,額外的耗時(shí)是什么呢棘利?
- 創(chuàng)建 Isolate
- Copy Message
當(dāng)我們按照上面的代碼執(zhí)行一段多線程代碼時(shí),經(jīng)歷了 isolate 的創(chuàng)建以及銷毀過程朽缴。下面是一種我們在解析 json 中這樣編寫代碼可能的方式善玫。
static BSModel toBSModel(String json){}
parsingModelList(List<String> jsonList) async{
for(var model in jsonList){
BSModel m = await compute(toBSModel, model);
}
}
復(fù)制代碼
在解析 json 的時(shí)候,我們可能通過 compute 把解析任務(wù)放在新的 isolate 中完成密强,然后把值傳過來茅郎。這時(shí)候我們會(huì)發(fā)現(xiàn),整個(gè)解析會(huì)變得異常的慢或渤。這是由于我們每次創(chuàng)建 BSModel
的時(shí)候都經(jīng)歷了一次 isolate 的創(chuàng)建以及銷毀過程系冗。這將會(huì)耗費(fèi)約 50-150ms 的時(shí)間。
在這之中薪鹦,我們傳遞 data 也經(jīng)歷了 Network -> Main Isolate -> New Isolate (result) -> Main Isolate掌敬,多出來兩次 copy 的操作惯豆。如果我們是在 Main 線程之外的 isolate 下載的數(shù)據(jù),那么就可以直接在該線程進(jìn)行解析奔害,最后只需要傳回 Main Isolate 即可楷兽,省下了一次 copy 操作。(Network -> New Isolate (result)-> Main Isolate)
空間
Isolate 實(shí)際上是比較重的华临,每當(dāng)我們創(chuàng)建出來一個(gè)新的 Isolate 至少需要 2mb 左右的空間甚至更多芯杀,取決于我們具體 isolate 的用途。
OOM 風(fēng)險(xiǎn)
我們可能會(huì)使用 message 傳遞 data 或 file雅潭。而實(shí)際上我們傳遞的 message 是經(jīng)歷了一次 copy 過程的揭厚,這其實(shí)就可能存在著 OOM 的風(fēng)險(xiǎn)。
如果說我們想要返回一個(gè) 2GB 的 data寻馏,在 iPhone X(3GB ram)上棋弥,我們是無法完成 message 的傳遞操作的。
Tips
上面已經(jīng)介紹了使用 isolate 進(jìn)行多線程操作會(huì)有一些額外的 cost诚欠,那么是否可以通過一些手段減少這些消耗呢。我個(gè)人建議從兩個(gè)方向上入手漾岳。
- 減少 isolate 創(chuàng)建所帶來的消耗轰绵。
- 減少 message copy 次數(shù),以及大小尼荆。
使用 LoadBalancer
如何減少 isolate 創(chuàng)建所帶來的消耗呢左腔。自然一個(gè)想法就是能否創(chuàng)建一個(gè)線程池,初始化到那里捅儒。當(dāng)我們需要使用的時(shí)候再拿來用就好了液样。
實(shí)際上 dart team 已經(jīng)為我們寫好一個(gè)非常實(shí)用的 package,其中就包括 LoadBalancer
巧还。
我們現(xiàn)在 pubspec.yaml 中添加 isolate 的依賴鞭莽。
isolate: ^2.0.2
復(fù)制代碼
然后我們可以通過 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
阶牍。下面我們再來看看應(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 使用感覺上差不多磕瓷,但是當(dāng)我們多次使用額外的 isolate 的時(shí)候盒齿,不再需要重復(fù)創(chuàng)建了。
并且 LoadBalancer
還支持 runMultiple,可以讓一個(gè)方法在多線程中執(zhí)行县昂。具體使用請查看 api肮柜。
LoadBalancer
經(jīng)過測試,它會(huì)在第一次使用其 isolate 的時(shí)候初始化線程池倒彰。
當(dāng)應(yīng)用打開后审洞,即使我們在頂層函數(shù)中調(diào)用了 LoadBalancer.create,但是還是只會(huì)有一個(gè) Isolate待讳。
當(dāng)我們調(diào)用 run 方法時(shí)芒澜,才真正創(chuàng)建出了實(shí)際的 isolate。