Flutter 真非同步

前行的烏龜發表於2019-10-28

Flutter 真非同步

前面說過 Flutter 實現非同步的方式有:async、awiteFutrue,但是這些本質還是 handle 佇列那套,更何況訊息佇列還是跑在 UI 執行緒裡的,要是你真有什麼耗時的操作放在這裡妥妥的光剩卡了。所以該開新執行緒還是得開,這裡我們來說說怎麼 new Isolate 和 Isolate 之間的通訊,但後再來看看系統給我們提供的簡便函式:compute


Isolate 的建立和 Isolate 之間的通訊

上文書 Isolate 之間是記憶體隔離的,單個 Isolate 內部是是以訊息佇列的方式執行的,這是 Isolate 的特徵。語言之間概念基本趨同,只是實現和細微不同,但是因概相同自然就會誕生相同的需求,Dart 自然提供了 Isolate 之間通訊的方式:port 埠,可以很方便的實現 Isolate 之間的雙向通訊,原理是向對方的佇列裡寫入任務

port 成對出現,分為:receivePort 接受埠SendPort 傳送埠receivePort 可以自己生成對應的 SendPort,只要把 SendPort 傳遞給對方就能實現從 SendPort->receivePort 的通許,當然這是單項的,雙向通訊的其實也一樣,對面把自己的 SendPort 傳過來就成了

var receivePort = ReceivePort();  
var sendPort = receivePort.sendPort;
複製程式碼

建立 Isolate 的 API 是:await Isolate.spawn(Function,SendPort),因為這是個非同步操作,所以加上 awaitFunction 這是個方法,是新的執行緒執行的核心方法,和 run 方法一樣的意思,SendPort 就是我們要傳給對面的通訊器

var anotherIsolate = await Isolate.spawn(otherIsolateInit, receivePort.sendPort);
複製程式碼

Isolate.listen 監聽方法我們可以拿到這個 Isolate 傳遞過來的資料,Isolate 之間什麼資料型別都可以傳遞,不必做任何標記,肯定是底層幫我們實現好了,很省事

receivePort.listen((date) {
    print("Isolate 1 接受訊息:data = $date");
});
複製程式碼

await receivePort.first 可以等待獲取第一個返回結果,但是不能和 receivePort.listen 一起寫,有衝突,只能選擇一個

final sendPort = await receivePort.first as SendPort;
複製程式碼

Isolate 關閉很直接,在 main isolate 中對其控制的 Isolate 呼叫 kill 方法就行了

void stop(){
print("kill isolate");
isolate?.kill(priority: Isolate.immediate);
isolate =null;
}
複製程式碼

不廢話了,上面很簡單的,把原理一說大家都明白,下面直接看程式碼:


Isolate 單向通訊

isolate 程式碼:

import 'dart:isolate';

var anotherIsolate;
var value = "Now Thread!";

void startOtherIsolate() async {
  var receivePort = ReceivePort();

  anotherIsolate = await Isolate.spawn(otherIsolateInit, receivePort.sendPort);

  receivePort.listen((date) {
    print("Isolate 1 接受訊息:data = $date,value = $value");
  });
}

void otherIsolateInit(SendPort sendPort) async {
  value = "Other Thread!";
  sendPort.send("BB");
}
複製程式碼

執行程式碼:

import 'DartLib.dart';

void main(){
  startOtherIsolate();
}
複製程式碼

結果:

Isolate 1 接受訊息:data = BB,value = Now Thread!
複製程式碼

Isolate 雙向通訊

isolate 程式碼:

import 'dart:isolate';

var anotherIsolate;
var value = "Now Thread!";

void startOtherIsolate() async {
  var receivePort = ReceivePort();
  var sendPort;

  anotherIsolate = await Isolate.spawn(otherIsolateInit, receivePort.sendPort);

  receivePort.listen((date) {
    if (date is SendPort) {
      sendPort = date as SendPort;
      print("雙向通訊建立成功");
      return;
    }
    print("Isolate 1 接受訊息:data = $date");
    sendPort.send("AA");
  });
}

void otherIsolateInit(SendPort sendPort) async {
  value = "Other Thread!";

  var receivePort = ReceivePort();
  print("Isolate 2 接受到來自 Isolate 1的port,嘗試建立同 Isolate 1的雙向通訊");

  receivePort.listen((date) {
    print("Isolate 2 接受訊息:data = $date");
  });

  sendPort.send(receivePort.sendPort);

  for (var index = 0; index < 10; index++) {
    sendPort.send("BB$index");
  }
}
複製程式碼

執行程式碼:

import 'DartLib.dart';

void main(){
  startOtherIsolate();
}
複製程式碼

執行結果:

Isolate 2 接受到來自 Isolate 1的port,嘗試建立同 Isolate 1的雙向通訊
雙向通訊建立成功
Isolate 1 接受訊息:data = BB0
Isolate 1 接受訊息:data = BB1
Isolate 1 接受訊息:data = BB2
Isolate 1 接受訊息:data = BB3
Isolate 1 接受訊息:data = BB4
Isolate 1 接受訊息:data = BB5
Isolate 1 接受訊息:data = BB6
Isolate 1 接受訊息:data = BB7
Isolate 1 接受訊息:data = BB8
Isolate 1 接受訊息:data = BB9
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
Isolate 2 接受訊息:data = AA
複製程式碼

系統 API:computer 函式

上面我們自己 new 一個 Isoalte 並實現通訊,多少有點麻煩,從封裝的角度看其中程式碼基本是重複的,所以 Google 就提供了一個 API 來幹這事:compute 方法

compute 方法是 Flutter 提供給我們的(記住不是 Dart),compute 內部會建立一個 Isolate 並返回計算結果,體驗上和一次性執行緒一樣,效能多少有些浪費,但是也有使用範圍

compute(function,value) compute 函式接受2個引數,第一個就是新執行緒的核心執行方法,第二個是傳遞過新執行緒的引數,可以是任何型別的資料,幾個也可以,但是要注意,function 函式的引數設計要和 value 匹配

compute 方法在 import 'package:flutter/foundation.dart' 這個包裡面

看個例子:

import 'dart:io';
import 'dart:isolate';
import 'package:flutter/foundation.dart';

void newTask() async {
  print("開始耗時計算,當前 isolate = ${Isolate.current.toString()}");
  var result = await compute(getName, "name");
  print(result);
}

String getName(String name) {
  print("正在獲取結果中...,當前 isolate = ${Isolate.current.toString()}");
  sleep(Duration(seconds: 2));
  return "Name";
}
複製程式碼

執行程式碼:

newTask();
複製程式碼
I/flutter (24384): 開始耗時計算,當前 isolate = Instance of 'Isolate'
I/flutter (24384): 正在獲取結果中...,當前 isolate = Instance of 'Isolate'
I/flutter (24384): Name
複製程式碼

compute 函式原始碼

compute 的原始碼不難,稍微用些新就能看懂,就是 new 了一個 isolate 出來,awite 第一個資料然後返回

//compute函式 必選引數兩個,已經講過了
Future<R> compute<Q, R>(ComputeCallback<Q, R> callback, Q message, { String debugLabel }) async {
  //如果是在profile模式下,debugLabel為空的話,就取callback.toString()
  profile(() { debugLabel ??= callback.toString(); });
  final Flow flow = Flow.begin();
  Timeline.startSync('$debugLabel: start', flow: flow);
  final ReceivePort resultPort = ReceivePort();
  Timeline.finishSync();
  //建立isolate,這個和前面講的建立isolate的方法一致
  //還有一個,這裡傳過去的引數是用_IsolateConfiguration封裝的類
  final Isolate isolate = await Isolate.spawn<_IsolateConfiguration<Q, R>>(
    _spawn,
    _IsolateConfiguration<Q, R>(
      callback,
      message,
      resultPort.sendPort,
      debugLabel,
      flow.id,
    ),
    errorsAreFatal: true,
    onExit: resultPort.sendPort,
  );
  final R result = await resultPort.first;
  Timeline.startSync('$debugLabel: end', flow: Flow.end(flow.id));
  resultPort.close();
  isolate.kill();
  Timeline.finishSync();
  return result;
}
複製程式碼

Isolate 使用場景

Isolate 雖好,但也有合適的使用場景,不建議濫用 Isolate,應儘可能多的使用Dart中的事件迴圈機制去處理非同步任務,這樣才能更好的發揮Dart語言的優勢

那麼應該在什麼時候使用Future,什麼時候使用Isolate呢?一個最簡單的判斷方法是根據某些任務的平均時間來選擇:

  • 方法執行在幾毫秒或十幾毫秒左右的,應使用Future
  • 如果一個任務需要幾百毫秒或之上的,則建議建立單獨的Isolate

除此之外,還有一些可以參考的場景

  • JSON 解碼
  • 加密
  • 影象處理:比如剪裁
  • 網路請求:載入資源、圖片

相關文章