我们小学二年级就学过:
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(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: [
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: [
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(blueCounter, redReceive.sendPort);
// 等待小蓝把发送器发送给小红
blueSender = await redReceive.first;
// 不用了记得关闭接收器
redReceive.close();
}
@override
void initState() {
controller =
AnimationController(duration: Duration(seconds: 3), vsync: this);
animation = Tween(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
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation animation;
String result = '';
SendPort blueSender;
Isolate isolate;
@override
void initState() {
controller =
AnimationController(duration: Duration(seconds: 3), vsync: this);
animation = Tween(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: [
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(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;
}