我們小學二年級就學過:
Dart 是單執行緒的並且 Flutter 依賴於 Dart。
但是如果我們要在主執行緒做一些計算量大的操作,就必然會阻塞執行緒,使UI更新卡頓甚至卡死。那怎麼辦呢?
好訊息是 Dart 為我們提供了 isolate,isolate 跟執行緒差不多,他是 Dart 中的執行緒。
isolate 與執行緒的區別就是執行緒與執行緒之間是共享記憶體的,而 isolate 和 isolate 之間是不共享的,所以叫 isolate (隔離)。
在flutter 裡面主執行緒就是主 isolate 。如果我們要進行一些大計算量的操作就應該啟動一個新的 isolate。
那麼應該如何來開啟呢?在此之前我想講個故事。
小紅與小藍的故事
有個舞者叫小紅,她正在給觀眾跳舞。
但是觀眾卻要求她一邊跳舞一邊計算一個數字裡面有多少個偶數。於是。。。
這那行啊!你必須給我一邊跳一邊算,算的時候不能停下來!
於是小紅沒辦法,決定在異世界召喚一個小藍來幫她計算。
但是小紅和小藍被異世界的屏障隔離,她們也沒有思想共通的超能力。只能在召喚的同時傳送一個包裹給小藍。
小藍被召喚出來後收到包裹,開啟后里面是要計算的數字,就開始計算,但是計算後要怎麼把結果告訴小紅呢?
上帝做了一個約定,在小紅召喚小藍的時候,會變一個傳送裝置(傳送裝置可以用來接收包裹,還可以生成一個專屬傳送器)。然後把傳送器傳送給小藍。
當小藍被召喚出來後,開啟包裹,裡面是一個傳送器,然後小藍自己也變一個傳送裝置,生成一個傳送器,然後用小紅的傳送器把小藍的傳送器傳送給小紅。傳送出去後就坐在傳送裝置旁邊等包裹。
當小紅收到小藍的傳送器後就把小藍的傳送器存起來。
當有觀眾要求小紅計算時,就分神一邊跳舞,一邊生成一個臨時傳送裝置,把要計算的數字和臨時傳送器打包成一個包裹,然後通過小藍的傳送器發給小藍,等傳送裝置出結果。因為不用自己算了,只是等,所以跳舞的時候線條也流暢了,動作也優美了。
說回小藍這邊,小藍看到傳送裝置出現了一個包裹,裡面是一個臨時傳送器,還有一個數字。於是小藍就開始計算。算好了就用臨時傳送器把數字傳送給小紅。
小紅收到結果後就告訴觀眾,那個數字有多少個偶數。
故事結束,第一次嘗試這樣的風格,可能寫得有點爛,不過結合程式碼來看的話,應該還是挺容易理解的。
程式碼實踐
首先我們先讓小紅跳起舞來。
@override
void initState() {
controller =
AnimationController(duration: Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: pi * 2).animate(controller);
controller.repeat();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedBuilder(
animation: animation,
child: Text(
'小紅',
style: TextStyle(fontSize: 30, color: Colors.red),
),
builder: (context, child) {
return Transform.rotate(
angle: animation.value,
child: child,
);
}),
],
),
),
);
}
複製程式碼
接下來讓小紅計算一個數字裡面有多少個偶數。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedBuilder(
animation: animation,
child: Text(
'小紅',
style: TextStyle(fontSize: 30, color: Colors.red),
),
builder: (context, child) {
return Transform.rotate(
angle: animation.value,
child: child,
);
}),
Padding(
padding: EdgeInsets.only(top: 16),
child:
RaisedButton(onPressed: count, child: Text('非同步計算偶數的個數')),
),
Text(result)
],
),
),
);
}
int getRandom() {
int a = Random().nextInt(100);
return a + 1000000000;
}
// 非同步計算
count() async {
int random = getRandom();
int r = countEven(random);
setState(() {
this.result = '${random.toString()}有${r.toString()}個偶數';
});
}
//計算偶數的個數
int countEven(num num) {
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
}
return count;
}
複製程式碼
這就是效果
定義 isolate
我願稱之為召喚小藍。
首先我們要知道兩個類:
ReceivePort
SendPort
複製程式碼
ReceivePort 就是故事中的傳送裝置,而 SendPort 則是傳送器。
我們可以通過以下方式建立傳送裝置和對應的傳送器
ReceivePort receive = ReceivePort();
SendPort sender = receive.sendPort;
複製程式碼
好的,知道這些就行了。接下來我們定義小藍。
// 訊息包裹,用來存臨時傳送器和訊息
class MessagePackage {
SendPort sender; // 臨時傳送器
dynamic msg; // 訊息
MessagePackage(this.sender, this.msg);
}
// 我是小藍,負責計算偶數的個數,我必須是頂級函式
blueCounter(SendPort redSendPort) {
// 建立小藍的傳送裝置
ReceivePort blueReceivePort = ReceivePort();
// 用小紅的傳送器把小藍的傳送器傳送出去
redSendPort.send(blueReceivePort.sendPort);
// 監聽小藍的傳送裝置,等待小紅叫小藍計算
blueReceivePort.listen((package) {
// 這裡的msg是dynamic,需要轉換成 MessagePackage 類,上面自己定義的包裹封裝類
MessagePackage _msg = package as MessagePackage;
// 小藍開始計算
int r = countEven(_msg.msg as num);
// 計算好了用小紅的臨時傳送器告訴小紅
_msg.sender.send(r);
});
}
複製程式碼
建立isolate
工具人小藍定義好了,我們去初始化(召喚)一下小藍。
// 建立isolate
void createIsolate() async {
// 建立小紅的接收器,用來接收小藍的傳送器
ReceivePort redReceive = ReceivePort();
// 建立 isolate, 並且把小紅的傳送器傳給小藍
isolate = await Isolate.spawn<SendPort>(blueCounter, redReceive.sendPort);
// 等待小藍把傳送器傳送給小紅
blueSender = await redReceive.first;
// 不用了記得關閉接收器
redReceive.close();
}
@override
void initState() {
controller =
AnimationController(duration: Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: pi * 2).animate(controller);
controller.repeat();
// 在initState中初始化isolate
createIsolate();
super.initState();
}
複製程式碼
現在小藍已經被召喚了出來,並且和小紅建立了通訊。
使isolate 開始計算
接下來我們就讓小紅開始計算吧。
@override
Widget build(BuildContext context) {
...
Padding(
padding: EdgeInsets.only(top: 16),
child: RaisedButton(
onPressed: isolateCount, child: Text('isolate計算偶數的個數')
),
),
...
}
// 開啟isolate計算
isolateCount() async {
// 獲取要計算的數字
int random = getRandom();
// 建立一個臨時傳送裝置
ReceivePort _temp = ReceivePort();
// 用小藍的傳送裝置傳送一個訊息包裹,裡面是臨時傳送裝置的傳送器和要計算的數字
blueSender.send(MessagePackage(_temp.sendPort, random));
// 等待臨時傳送裝置返回計算結果
int r = await _temp.first;
// 不用了記得關閉臨時接收器
_temp.close();
// 把計算結果告訴觀眾
setState(() {
this.result = '${random.toString()}有${r.toString()}個偶數';
});
}
複製程式碼
需要注意的是當使用完了 isolate 記得要銷燬。
@override
void dispose() {
// 銷燬 isolate
isolate?.kill(priority: Isolate.immediate);
super.dispose();
}
複製程式碼
OK,到這裡相信你已經看懂並會使用 isolate 了。 我們來看看效果圖。
使用 computed
到這裡還沒完,也許你會覺得太麻煩了。是的這樣用 isolate 太麻煩了,isolate 被設計成可以多次輸入輸出,而我們做這個計算只有一次輸入和輸出,那麼我們就可以用 flutter 為我們提供的 computed 來完成計算操作,它是對 isolate 的一個封裝。下面看看怎麼用吧!敲簡單的。
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'isolate Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'isolate Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
String result = '';
SendPort blueSender;
Isolate isolate;
@override
void initState() {
controller =
AnimationController(duration: Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: pi * 2).animate(controller);
controller.repeat();
// 在initState中初始化isolate
createIsolate();
super.initState();
}
@override
void dispose() {
// 銷燬 isolate
isolate?.kill(priority: Isolate.immediate);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedBuilder(
animation: animation,
child: Text(
'小紅',
style: TextStyle(fontSize: 30, color: Colors.red),
),
builder: (context, child) {
return Transform.rotate(
angle: animation.value,
child: child,
);
}),
Padding(
padding: EdgeInsets.only(top: 16),
child: RaisedButton(onPressed: count, child: Text('非同步計算偶數的個數')),
),
Padding(
padding: EdgeInsets.only(top: 16),
child: RaisedButton(
onPressed: isolateCount, child: Text('isolate計算偶數的個數')),
),
Padding(
padding: EdgeInsets.only(top: 16),
child: RaisedButton(
onPressed: computeCount, child: Text('compute計算偶數的個數')),
),
Text(result)
],
),
),
);
}
// 獲取隨機數
int getRandom() {
int a = Random().nextInt(100);
return a + 1000000000;
}
// 非同步計算
count() async {
int random = getRandom();
int r = countEven(random);
setState(() {
this.result = '${random.toString()}有${r.toString()}個偶數';
});
}
// 建立isolate
void createIsolate() async {
// 建立小紅的接收器,用來接收小藍的傳送器
ReceivePort redReceive = ReceivePort();
// 建立 isolate, 並且把小紅的傳送器傳給小藍
isolate = await Isolate.spawn<SendPort>(blueCounter, redReceive.sendPort);
// 等待小藍把傳送器傳送給小紅
blueSender = await redReceive.first;
// 不用了記得關閉接收器
redReceive.close();
}
// 利用compute計算
computeCount() async {
int random = getRandom();
// compute 的回撥函式必須是頂級函式或者static函式
int r = await compute(countEven, random);
setState(() {
this.result = '${random.toString()}有${r.toString()}個偶數';
});
}
// 開啟isolate計算
isolateCount() async {
// 獲取要計算的數字
int random = getRandom();
// 建立一個臨時傳送裝置
ReceivePort _temp = ReceivePort();
// 用小藍的傳送裝置傳送一個訊息包裹,裡面是臨時傳送裝置的傳送器和要計算的數字
blueSender.send(MessagePackage(_temp.sendPort, random));
// 等待臨時傳送裝置返回計算結果
int r = await _temp.first;
_temp.close();
// 把計算結果告訴觀眾
setState(() {
this.result = '${random.toString()}有${r.toString()}個偶數';
});
}
}
// 訊息包裹,用來存臨時傳送器和訊息
class MessagePackage {
SendPort sender; // 臨時傳送器
dynamic msg; // 訊息
MessagePackage(this.sender, this.msg);
}
// 我是小藍,負責計算偶數的個數,我必須是頂級函式
blueCounter(SendPort redSendPort) {
// 建立小藍的傳送裝置
ReceivePort blueReceivePort = ReceivePort();
// 用小紅的傳送器把小藍的傳送器傳送出去
redSendPort.send(blueReceivePort.sendPort);
// 監聽小藍的傳送裝置,等待小紅叫小藍計算
blueReceivePort.listen((package) {
// 這裡的msg是dynamic,需要轉換成 MessagePackage 類,上面自己定義的包裹封裝類
MessagePackage _msg = package as MessagePackage;
// 小藍開始計算
int r = countEven(_msg.msg as num);
// 計算好了用小紅的臨時傳送器告訴小紅
_msg.sender.send(r);
});
}
//計算偶數的個數,此函式需要大量的計算資源和時間
int countEven(num num) {
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
}
return count;
}
複製程式碼