flutter“多线程”isolate小学生级理解

2,903 阅读8分钟

我们小学二年级就学过:

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 了。 我们来看看效果图。

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;
}