flutter入門(mén)之理解Isolate及compute

一 . 原始代碼
為什么要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í)候初始化線程池。

image.png

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

image.png

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容