flutter入门之理解Isolate及compute

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

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
 
class TestWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return TestWidgetState();
  }
}
 
class TestWidgetState extends State<TestWidget> {
  int _count = 0;
 
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          children: <Widget>[
            Container(
              width: 100,
              height: 100,
              child: CircularProgressIndicator(),
            ),
            FlatButton(
                onPressed: () async {
                  _count = countEven(1000000000);
                  setState(() {});
                },
                child: Text(
                  _count.toString(),
                )),
          ],
          mainAxisSize: MainAxisSize.min,
        ),
      ),
    );
  }
 
  //计算偶数的个数
  static int countEven(int num) {
    int count = 0;
    while (num > 0) {
      if (num % 2 == 0) {
        count++;
      }
      num--;
    }
    return count;
  }
}

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



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

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

a. 将count改为asyncCountEven
  static Future<int> asyncCountEven(int num) async{
    int count = 0;
    while (num > 0) {
      if (num % 2 == 0) {
        count++;
      }
      num--;
    }
    return count;
  }
b. 调用:
_count = await asyncCountEven(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(countEven, 1000000000);
再次运行,我们来看看效果吧:



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

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

 a. 增加这两个函数
  static Future<dynamic> isolateCountEven(int num) async {
    final response = ReceivePort();
    await Isolate.spawn(countEvent2, response.sendPort);
    final sendPort = await response.first;
    final answer = ReceivePort();
    sendPort.send([answer.sendPort, num]);
    return answer.first;
  }
 
  static void countEvent2(SendPort port) {
    final rPort = ReceivePort();
    port.send(rPort.sendPort);
    rPort.listen((message) {
      final send = message[0] as SendPort;
      final n = message[1] as int;
      send.send(countEven(n));
    });
  }
b. 使用
_count = await isolateCountEven(1000000000);
相对于compute复杂了很多,效果就不贴了,和compute一样,毫无卡顿。。

代价是什么

对于我们来说,其实是把多线程当做一种计算资源来使用的。我们可以通过创建新的 isolate 计算 heavy work,从而减轻 UI 线程的负担。但是这样做的代价是什么呢?

时间

通常来说,当我们使用多线程计算的时候,整个计算的时间会比单线程要多,额外的耗时是什么呢?

  • 创建 Isolate
  • Copy Message

当我们按照上面的代码执行一段多线程代码时,经历了 isolate 的创建以及销毁过程。下面是一种我们在解析 json 中这样编写代码可能的方式。

  static BSModel toBSModel(String json){}

  parsingModelList(List<String> jsonList) async{
    for(var model in jsonList){
      BSModel m = await compute(toBSModel, model);
    }
  }
复制代码

在解析 json 的时候,我们可能通过 compute 把解析任务放在新的 isolate 中完成,然后把值传过来。这时候我们会发现,整个解析会变得异常的慢。这是由于我们每次创建 BSModel 的时候都经历了一次 isolate 的创建以及销毁过程。这将会耗费约 50-150ms 的时间。

在这之中,我们传递 data 也经历了 Network -> Main Isolate -> New Isolate (result) -> Main Isolate,多出来两次 copy 的操作。如果我们是在 Main 线程之外的 isolate 下载的数据,那么就可以直接在该线程进行解析,最后只需要传回 Main Isolate 即可,省下了一次 copy 操作。(Network -> New Isolate (result)-> Main Isolate)

空间

Isolate 实际上是比较重的,每当我们创建出来一个新的 Isolate 至少需要 2mb 左右的空间甚至更多,取决于我们具体 isolate 的用途。

OOM 风险

我们可能会使用 message 传递 data 或 file。而实际上我们传递的 message 是经历了一次 copy 过程的,这其实就可能存在着 OOM 的风险。

如果说我们想要返回一个 2GB 的 data,在 iPhone X(3GB ram)上,我们是无法完成 message 的传递操作的。

Tips

上面已经介绍了使用 isolate 进行多线程操作会有一些额外的 cost,那么是否可以通过一些手段减少这些消耗呢。我个人建议从两个方向上入手。

  • 减少 isolate 创建所带来的消耗。
  • 减少 message copy 次数,以及大小。

使用 LoadBalancer

如何减少 isolate 创建所带来的消耗呢。自然一个想法就是能否创建一个线程池,初始化到那里。当我们需要使用的时候再拿来用就好了。

实际上 dart team 已经为我们写好一个非常实用的 package,其中就包括 LoadBalancer

我们现在 pubspec.yaml 中添加 isolate 的依赖。

isolate: ^2.0.2
复制代码

然后我们可以通过 LoadBalancer 创建出指定个数的 isolate。

Future<LoadBalancer> loadBalancer = LoadBalancer.create(2, IsolateRunner.spawn);
复制代码

这段代码将会创建出一个 isolate 线程池,并自动实现了负载均衡。

由于 dart 天生支持顶层函数,我们可以在 dart 文件中直接创建这个 LoadBalancer。下面我们再来看看应该如何使用 LoadBalancer 中的 isolate。

 int useLoadBalancer() async {
    final lb = await loadBalancer;
    int res = await lb.run<int, int>(_doSomething, 1);
    return res;
  }
复制代码

我们关注的只有 Future<R> run<R, P>(FutureOr<R> function(P argument), argument, 方法。我们还是需要传入一个 function 在某个 isolate 中运行,并传入其参数 argument。run 方法将会返回我们执行方法的返回值。

整体和 compute 使用感觉上差不多,但是当我们多次使用额外的 isolate 的时候,不再需要重复创建了。

并且 LoadBalancer 还支持 runMultiple,可以让一个方法在多线程中执行。具体使用请查看 api。

LoadBalancer 经过测试,它会在第一次使用其 isolate 的时候初始化线程池。

image.png

当应用打开后,即使我们在顶层函数中调用了 LoadBalancer.create,但是还是只会有一个 Isolate。

image.png

当我们调用 run 方法时,才真正创建出了实际的 isolate。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343