Flutter的Isolate及compute基本使用

一、前言

原文:《flutter入门之理解Isolate及compute》 作者:

这篇文章将会讲解flutter中的Isolate,这有助于帮你解决某些耗时计算问题导致的卡顿。

本文示例代码:https://gitee.com/tilongzs/flutter_study_demo/tree/master/isolate_test

二、原始代码

为什么要Isolate,我们先看一段比较简单的代码:

import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp>{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title :'App',
      initialRoute: '/',
      routes: {
        '/':(context)=>HomePage(),
      },
    );
  }
}

class HomePage extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    return _HomePageState();
  }
}

class _HomePageState extends State<HomePage>{
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('App'),
      ),
      body: Center(
        child: Column(
          children: [
            Container(
              width: 100,
              height: 100,
              child: CircularProgressIndicator(),
            ),
            SizedBox(
              width: 200,
              height: 50,
              child: RaisedButton(
                  color: Colors.amberAccent,
                  onPressed: () async{
                    _count = countFunc(1000000000);
                    setState(() { });
                  },
                  child: Text(_count.toString())),
            )
          ],
        ),
      ),
    );
  }

  //计算偶数的个数
  static int countFunc(int num) {
    int count = 0;
    while (num > 0) {
      if (num % 2 == 0) {
        count++;
      }
      num--;
    }
    return count;
  }
}

UI包含两个部分,一个不断转圈的progress指示器,一个按钮,当点击按钮的时候,找出比某个正整数n小的数的偶数的个数(请忽视具体算法,故意做耗时计算用,哈哈)。我们来运行一下代码看看效果:

可以看到,本来是很流畅的转圈,当我点击按钮计算的时候,UI出现了卡顿。为什么会出现卡顿,因为我们的计算默认是在UI线程中的,当我们调用countFunc的时候,这个计算需要耗时,而在这期间,UI是没有机会去调用刷新的,因此会卡顿,计算完成后,UI恢复正常刷新。

三、使用async优化

那么有些同学就会说了,在dart中,有async关键字,我们可以用异步计算,这样就不会影响UI的刷新了,事实真的是这样吗?我们一起来修改一下代码:

  //计算偶数的个数
  static Future<int> countFunc(int num) async{
    int count = 0;
    while (num > 0) {
      if (num % 2 == 0) {
        count++;
      }
      num--;
    }
    return count;
  }

调用:

_count = await countFunc(1000000000);

我们继续运行一下代码,仍然卡顿,说明异步是解决不了问题的,为什么?因为我们仍旧是在同一个UI线程中做运算,异步只是说我可以先运行其他的,等我这边有结果再返回。但是,记住,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。

四、使用compute优化

那么我们怎么解决这个问题呢,其实很简单,我们知道卡顿的原因是在同一个线程中导致的,那我们有没有办法将计算移到新的线程中呢,当然是可以的。不过在dart中,这里不是称呼线程,是Isolate,直译叫做隔离,这么古怪的名字,是因为隔离不共享数据,每个隔离中的变量都是不同的,不能相互共享。

但是由于dart中的Isolate比较重量级,UI线程和Isolate中的数据的传输比较复杂,因此flutter为了简化用户代码,在foundation库中封装了一个轻量级compute操作,我们先看看compute,然后再来看Isolate。

要使用compute,必须注意的有两点,一是我们的compute中运行的函数,必须是顶级函数或者是static函数,二是compute传参,只能传递一个参数,返回值也只有一个,我们先看看本例中的compute优化吧:

只用在使用的时候,放到compute函数中就行了。

_count = await compute(countFunc, 1000000000);

再次运行,我们来看看效果吧:

可以看到,现在的计算并不会导致UI卡顿,完美解决问题。

五、使用Isolate优化

但是,compute的使用还是有些限制,它没有办法多次返回结果,也没有办法持续性的传值计算,每次调用,相当于新建一个隔离,如果调用过多的话反而会适得其反。在某些业务下,我们可以使用compute,但是在另外一些业务下,我们只能使用dart提供的Isolate了,我们先看看Isolate在本例中的使用:

1、增加这两个函数

static Future<dynamic> isolateCountFunc(int num) async {
  // 获取处理函数的SendPort
  final receivePort = ReceivePort();
  await Isolate.spawn(isolateCountProc, receivePort.sendPort);
  final procPort = await receivePort.first; //  获取到处理函数的SendPort,receivePort不能继续使用

  // 向处理函数发送处理请求
  final answer = ReceivePort();
  procPort.send([answer.sendPort, num]);
  return answer.first;
}

// 处理函数
static void isolateCountProc(SendPort port) {
  var countFunc = (int num){
    int count = 0;
    while (num > 0) {
      if (num % 2 == 0) {
        count++;
      }
      num--;
    }
    return count;
  };

  final rPort = ReceivePort();
  rPort.listen((message) {
    final send = message[0] as SendPort;
    final n = message[1] as int;
    send.send(countFunc(n));
  });

  port.send(rPort.sendPort);
}

2、使用

_count = await isolateCountFunc(1000000000);

相对于compute复杂了很多,和compute一样,毫无卡顿。

六、扩展

isolate使用这么复杂,那么我们在那些情况下使用它呢?

想象一下,假如你有一个业务,是使用socket和服务器连接,不断的从服务器中读取tcp流,如果业务需要拆分包的话,我们需要不断的将读取到的tcp流进行拆包分包,然后使用获取的数据来更新UI。首先,我们的socket与服务器通信,如果服务器推送消息过快,那么肯定会出现上面的情况,socket这边一直阻塞线程,导致UI刷新卡顿,所以我们需要将socket这边的业务放到隔离里面,但是,compute函数的限制我们也说了,它基本是是一次运行一次返回,但是业务要求是通过tcp的长连接不断获取服务器的数据,所以,这里我们就只能使用isolate,在UI和isolate里面使用ReceivePort进行双向通信,这样才能保证UI不卡顿的情况下仍然保持业务的完整性。

留下评论