Flutter —— 多线程

1,363 阅读6分钟

这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

1. 多线程

1.1. isolate

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。应用程序启动后,开始执行main函数并运行main。isolate。

下面的打印结果一定是123,因为这里Future会等待主线程睡眠结束。

void IsolateDemo() {
  print('1');
  Future(func);
  sleep(Duration(seconds: 2));
  print('2');
}

FutureOr func() {
  print('3');
}

那么如果将func放在Isolate里面执行,那么在打印2之前func就会先打印3。

void IsolateDemo() {
  print('1');

  Isolate.spawn(func,10);
  sleep(Duration(seconds: 1));
  print('2');
}


void func(int message) {
  print('3');
}

在这里插入图片描述

多添加几次任务来执行。

void IsolateDemo() {
  print('外部代码1');

  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  sleep(Duration(seconds: 1));
  print('外部代码2');
}

运行后发现这里任务一不一定比任务2先执行,而且外部代码也会在任务一任务二之前执行,证明了这里Isolate确实是子线程里面执行任务的。

在这里插入图片描述

Dart中的isolate更像是一个进程,因为isolate有独立的内存空间,意味着每个isolate中的数据是独立的,所以没有资源抢夺的问题,也就不需要锁。所以,我们访问数据不能直接访问。isolate相当于轻量级的进程,他不会独立开辟堆和栈,而是给了一个局部的内存空间,所有的变量,内存对象都在这个空间里面,和原来的程序传递数据的时候就需要用到进程间的通讯。 声明一个属性没然后在func里面赋值,在IsolateDemo里面睡眠之后打印a的值看一下是否改变。

int a = 10;
void IsolateDemo() {
  print('外部代码1');

  Isolate.spawn(func,1000);

  sleep(Duration(seconds: 1));
  print('外部代码来了a=$a');
  print('外部代码2');
}

void func(int message) {
  a = message;
  print('第一个来了a=$a');
}

运行后发现是没有改变的

在这里插入图片描述

那么如果非要改变a的值的话,那么就需要用到port,将port的sendPort作为参数传给func,然后添加listen监听数据变化来接受func里面传过来的值。

void IsolateDemo() {
  print('外部代码1');
  //  创建一个port
  ReceivePort port = ReceivePort();
// 创建一个Isolate
  Isolate.spawn(func,port.sendPort);
  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了a=$a');
  });

  sleep(Duration(seconds: 1));
  print('外部代码来了a=$a');
  print('外部代码2');
}


void func(SendPort send) {
  send.send(100);
  print('第一个来了a=$a');
}

运行后发现a的值改变了。

在这里插入图片描述

既然我们Isolate开辟了空间,那么我们就要手动去销毁Isolate。 这里创建临时变量iso,然后在port.listen里面关闭端口以及杀掉iso。这里的await不会堵塞后面代码的执行,因为这里是其他线程里面的。

void IsolateDemo() async {
  print('外部代码1');
  //  创建一个port
  ReceivePort port = ReceivePort();
  // 创建一个Isolate
 Isolate iso =  await Isolate.spawn(func,port.sendPort);

  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了a=$a');
    port.close();
    iso.kill();
  });
  print('外部代码来了a=$a');
  print('外部代码2');
}

1.2 compute

关于Iso还有一个封装叫compute,这里使用compute之后发现这里并不会堵塞住compute的执行。

void computeTest() {
  print('外部代码1');
  compute(func1,10);
  sleep(Duration(seconds: 2));
  print('外部代码2');
}
FutureOr func1(message) {
  print('compute');
}

运行结果:

在这里插入图片描述

compute和iso不一样的是,如果加了await,那么后面的代码就需要等待。并且compute可以返回数据来修改数据的值,但是如果在func1修改a的值的话同样是没有效果的。

void computeTest() async{
  print('外部代码1');
  a = await compute(func1,10);
  print('外部代码2 a = $a');
}
int func1(message) {
  sleep(Duration(seconds: 2));

  print('compute');
  return 1000;
}

在这里插入图片描述

2. 异步和多线程的结合

下面的打印是同步还是异步呢?

void isoLoadDemo() {
  Future( () => compute(testfunc,123)).then((value) => {print('1结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('2结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('3结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('4结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('5结束')});

}

运行后看到这里是异步的。

在这里插入图片描述

那么如果是这样的呢?

void isoLoadDemo() {
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('1结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('2结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('3结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('4结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('5结束')});
}

运行后发现这里是同步的了。

在这里插入图片描述

这是为什么呢?因为箭头函数其实是return compute(testfunc, 123) 的,如果在下面的代码添加return之后,那么就是异步的了。如果返回的是子线程的Future,那么.then处理的是子线程的Future的结果,否则就是当前Future的结果。 那么 compute(testfunc, 123) 的执行是有序的还是无序的呢?在Future里面添加打印

void isoLoadDemo() {
  Future(() {
    print('1开始');
    return compute(testfunc, 123);
  }).then((value) => {print('1结束')});
  Future(() {
    print('2开始');
    return compute(testfunc, 123);
  }).then((value) => {print('2结束')});
  Future(() {
    print('3开始');
    return  compute(testfunc, 123);
  }).then((value) => {print('3结束')});
  Future(() {
    print('4开始');
    return compute(testfunc, 123);
  }).then((value) => {print('4结束')});
  Future(() {
    print('5开始');
    return compute(testfunc, 123);
  }).then((value) => {print('5结束')});
}

运行后发现开始是按顺序打印的,那么就说明compute(testfunc, 123)是按顺序执行的,但是返回的时机是无序的。

在这里插入图片描述

那么同理可知,如果去掉return, 那么就会按顺序打印1开始,1结束 ...... ,运行后证明是正确的。

在这里插入图片描述

其实.then任务和Future可以当作是一个整体。下面代码中,按道理应该是Future里面的微任务1先被添加到队列中,然而实际上却是微任务先执行,所以说这里可以把.then和Future当作是一个整体。

 Future x = Future((){
    print('异步任务1');
    scheduleMicrotask(() {
      print('微任务1');
    });
  });

  x.then((value) {
    print('微任务2');
  });

在这里插入图片描述

这里whenComplete也是一样的,并且如果whenComplete在then前面,那么就先于.then执行,否则就后于.then执行。

在这里插入图片描述

在这里插入图片描述

4. Timer

Timer会默认开启一个异步任务。

void main() {

  Timer.run(() {
    print('异步任务');
  });

  print('来了');
  }

运行后 在这里插入图片描述 回到之前的聊天界面,试一下timer是否会卡住ui。 在initState里面添加一个timer

  int _count = 0;
    Timer.periodic(Duration(seconds: 1), (timer) {
      _count ++;
      print('$_count');
      if (_count == 99) {
        timer.cancel();
      }
    });

然后在timer启动后拖动ListView,发现timer是没有像ios里面一样是暂停的。但是这里有一个小问题,之前聊天界面是保存状态的,如果没有保存状态的话,那么重新进入聊天页面,就会重新走一个init,重新创建一个timer,而前面的timer没有被取消,那么就会有多个tiemr存在的情况。

在这里插入图片描述

这个时候就需要用到dispose,当State 被永久的从视图树中移除时,Flutter 会调用dispose 函数。 添加一个变量timer,然后给timer赋值。

  late Timer _timer;
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      _count ++;
      print('$_count');
      if (_count == 99) {
        timer.cancel();
      }
    });

在dispose中判断如果_timer有值且正在运行,那么就将其取消掉。

 @override
  void dispose() {
    // TODO: implement dispose

    if (_timer != null && _timer.isActive) {
      _timer.cancel()
    }

    super.dispose();
  }

5. 多线程和异步使用时机

什么时候该使用多线程,什么时候该使用异步呢? 在聊天界面的AppBar的actions添加一个GestureDetector,里面是一个添加按钮,然后在onTap里面添加一个异步的耗时操作。

 GestureDetector(
            child: Container(
              child: Icon(Icons.add),
            ),
            onTap: () {
              Future(() {
                print('开始');
                for (int i = 0; i < 1000000000; i++) {
                }
               print("结束了");
              });
            },
          ),

点击后发现主线程完全被卡死了,页面滑动不了,并且timer也不动了。这个时候就需要将耗时操作放在子线程执行,这样主线程就不会被卡住了。

onTap: () {
              Future(() {
               return compute(func,123);
              });
            },
 FutureOr func(int message) {
    print('开始');
    for (int i = 0; i < 1000000000; i++) {
    }
    print("结束了");
  }

2. 总结:

  • Dart中的多线程
    • Dart是单线程语言,但并不代表它不能并行执行代码。因为它拥有Isolate。
    • Isolate
      • Isolate可以看成是一个小的进程。
        • 它拥有独立的内存空间,不同Isolate之间通过消息进行通信
        • 它拥有自己的事件循环及队列(MicroTask 和 Event)
      • Isolate使用
        • 1、创建子线程任务:Isolate.spawn(arg1,arg2);
          • arg1: 需要在子线程执行的函数
          • arg2:传递给函数的参数
          • 这样就在另一个线程中执行arg1的代码了。
        • 2、端口通讯
          • ReceivePort port = ReceivePort()//构造一个端口。
          • port.listen(arg)//注册一个回调监听
            • arg为回调函数。参数为消息内容
          • 在子线程中.通过port.send() 发送消息
        • 3、关闭端口,销毁Isolate
          • 注意端口使用完毕需要调用port.close()函数关闭
          • Isolate使用完毕,需要调用Isolate.kill()函数销毁
    • compute
      • 由于dart中的Isolate比较复杂,数据传输比较麻烦,因此flutter在foundation库中封装了一个轻量级compute操作
      • 使用:compute(func,count)
        • func:子线程函数!func如果有返回值会直接被compute函数返回出去!
        • count: 给函数的参数
  • 异步多线程结合
    • Dart中的异步是可以和多线程结合使用的。
    • 如果Future中返回子线程的返回值。那么Future的处理是异步的
    • 如果Future中没有返回子线程的返回值。那么Future的处理是同步的
    • Future的结果处理会在Future执行完毕立即执行。可以看做是一个任务。