Flutter | 性能优化——如何避免应用 jank

前言

流畅的用户体验一直是每一位开发者的不断追求,为了让本身的应用是否能给用户带来持续的高帧率渲染体验,咱们天然想要极力避免发生 jank(卡顿,不流畅)。html

本文将会解释为何即便在 Flutter 高性能的渲染能力下,应用仍是可能会出现 jank,以及咱们应该如何处理这些状况。这是 Flutter 性能分析系列的第一篇文章,后续将会持续剖析 Flutter 中渲染流程以及性能优化。数据库

何时会产生 jank?

我见过许多开发者在刚上手了 Flutter 以后,尝试开发了一些应用,然而并无取得比较好的性能表现。例如在长列表加载的时候可能会出现明显卡顿的状况(固然这并不常见)。当你对这种状况没有头绪的时候,可能会误觉得是 Flutter 的渲染还不够高效,然而大几率是你的 姿式不对。咱们来看一个小例子。json

在屏幕中心有一个一直旋转的 FlutterLogo,当咱们点击按钮后,开始计算 0 + 1 + ... +1000000000。这里能够很明显的感觉到明显的卡顿。为何会出现这种状况呢?api

Flutter Rendering Pipeline

Flutter 由 GPU 的 vsync 信号驱动,每一次信号都会走一个完整的 pipeline(咱们如今并不须要关心整个流程的具体细节),而一般咱们开发者会接触到的部分就是使用 dart 代码,通过 build -> layout -> paint 最后生成一个 layer,整个过程都在一个 UI 线程中完成。Flutter 须要在每秒60次,也就是 16.67 ms 经过 vsync 进行一次 pipline。性能优化

在 Android 中咱们是不能在 主线程(UI线程)中进行耗时操做的,若是作一些比较繁重的操做,好比网络请求、数据库操做等相关操做,就会致使 UI 线程卡住,触发 ANR。因此咱们须要把这些操做放在子线程去作,经过 handler/looper/message queue 三板斧把结果传给主线程。而 dart 天生是单线程模式,为何咱们可以轻松的作这些任务,而不须要另开一个线程呢?网络

熟悉 dart 的同窗确定了解 event loop 机制了,经过异步处理咱们能够把一个方法在执行过程当中暂停,首先保证咱们的同步方法可以按时执行(这也是为何 setState 中只能进行同步操做的缘故)。而整个 pipline 是一次同步的任务,因此异步任务就会暂停,等待 pipline 执行结束,这样就不会由于进行耗时操做卡住 UI。多线程

可是单线程毕竟也有它的局限,可是当咱们有一些比较重的同步处理任务,例如解析大量 json(这是一个同步操做),或是处理图片这样的操做,极可能处理时间会超过一个 vsync 时间,这样 Flutter 就不能及时将 layer 送到 GPU 线程,致使应用 jank。负载均衡

在上面这个例子中,咱们经过计算 0 + 1 + ... +1000000000 来模拟一个耗时的 json 解析操做,因为它是一个同步的行为,因此它的计算不会被暂停。咱们这个复杂的计算任务耗时超过了一次 sync 时间,因此产生了明显的 jank。异步

int doSomeHeavyWork() {
    int res = 0;
    for (int i = 0; i <= 1000000000; i++) {
      res += i;
    }
    return res;
  }
复制代码

如何解决

既然 dart 单线程没法解决这样的问题,咱们很容易就会想到使用多线程解决这个问题。在 dart 中,它的线程概念被称为 isolate。async

它与咱们以前理解的 Thread 概念有所不一样,各个 isolate 之间是没法共享内存空间,isolate 之间有本身的 event loop。咱们只能经过 Port 传递消息,而后在另外一个 isolate 中处理而后将结果传递回来,这样咱们的 UI 线程就有更多余力处理 pipeline,而不会被卡住。更多概念性的描述请参考 isolate API文档

建立一个 isolate

咱们能够经过 Isolate.spawn 建立一个 isolate。

static Future<Isolate> spawn<T>(void entryPoint(T message),T message);
复制代码

当咱们调用 Isolate.spawn 的时候,它将会返回一个对 isolate 的引用的 Future。咱们能够经过这个 isolate 来控制建立出的 Isolate,例如 pause、resume、kill 等等。

  • entryPoint:这里传入咱们想要在其余 isolate 中执行的方法,入参是一个任意类型的 message。entryPoint 只能是顶层方法或静态方法,且返回值为 void。
  • message:建立 Isolate 第一个调用方法的入参,能够是任意值。

可是在此以前咱们必需要建立两个 isolate 之间沟通的桥梁。

ReceivePort / SendPort

在两个 isolate 之间,咱们必须经过 port 来传递 message。ReceivePort 与 SendPort 就像是一部单向通讯电话。ReceivePort 自带一部 SendPort,当咱们建立 isolate 的时候,就把 ReceivePort 的 SendPort 丢给建立出来的 isolate。当新的 isolate 完成了计算任务时,经过这个 sendPort 去 send message。

static void _methodRunAnotherIsolate(dynamic message) {
    if (message is SendPort) {
      message.send('Isolate Created!');
    }
  }
复制代码

这里假设先有一个须要在其余 isolate 中执行的方法,入参是一个 SendPort。须要注意的是,这里的方法只能是顶层方法或静态方法,因此咱们这里使用了 static 修饰,并让其变成一个私有方法("_")。它的返回值也只能是 void,你可能会问,那咱们如何得到结果呢?

还记得咱们刚才建立的 ReceivePort 吗。是的,如今咱们就须要监听这个 ReceivePort 来得到 sendPort 传递的 message。

createIsolate() async {
    ReceivePort receivePort = ReceivePort();
    try {
    // create isolate
      isolate =
          await Isolate.spawn(_methodRunAnotherIsolate, receivePort.sendPort);
          
    // listen message from another isolate 
      receivePort.listen((dynamic message) {
          print(message.toString());
      });
    } catch (e) {
      print(e.toString());
    } finally {
      isolate.addOnExitListener(receivePort.sendPort,
          response: "isolate has been killed");
    }
    isolate?.kill();
  }
复制代码

咱们先建立出 ReceivePort,而后在 Isolate.spawn 的时候将 receivePort.sendPort 做为 message 传入新的 isolate。

而后监听 receivePort,并打印收听到的 message。这里须要注意的是,咱们须要手动调用 isolate?.kill() 来关闭这个 isolate。

输出结果:

flutter: Isolate Created!

flutter: isolate has been killed

实际上这里不写 isolate?.kill() 也会在 gc 时自动销毁 isolate。

这时候你可能会问,咱们的 entryPoint 只容许有一个入参,若是咱们想要执行的方法须要传入其余参数怎么办呢。

定义协议

其实很简单,咱们定义一个协议就好了。好比像下面这样咱们定义一个 SpawnMessageProtocol 做为 message。

class SpawnMessageProtocol{
  final SendPort sendPort;
  final String url;
  SpawnMessageProtocol(this.sendPort, this.url);
}
复制代码

协议中包含 SendPort 便可。

更方便的 Compute

刚才咱们使用的 Isolate.spawn 建立 Isolate 天然会以为太过复杂,有没有一种更好的方式呢。实际上 Flutter 已经为咱们封装了一些实用方法,让咱们可以更加天然地使用多线程进行处理。这里咱们先建立一个须要在其余 isolate 中运行的方法。

static int _doSomething(int i) {
    return i + 1;
  }
复制代码

而后使用 compute 在另外一个 isolate 中执行该方法,并返回结果。

runComputeIsolate() async{
      int i = await compute(_doSomething, 8);
      print(i);
  }
复制代码

仅仅一行代码咱们就可以让 _doSomething 运行在另外一个 isolate 中,并返回结果。这种方式对使用者来讲几乎没有负担,基本上和写异步代码是同样的。

代价是什么

对于咱们来讲,实际上是把多线程当作一种计算资源来使用的。咱们能够经过建立新的 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 的时候初始化线程池。

当应用打开后,即便咱们在顶层函数中调用了 LoadBalancer.create,可是仍是只会有一个 Isolate。

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

写在最后

写这篇文章的缘故实际上是前两天法空大佬在作图片处理的时候恰好遇到了这个问题,他最后仍是调用原生的库解决的,不过我仍是写一篇,给以后遇到这个问题的同窗一种参考方案。

固然 Flutter 中性能调优远不止这一种状况,build / layout / paint 每个过程其实都有不少可以优化的细节,这个会在以后性能优化系列跟你们慢慢分享。

最近很长一段时间其实在学习混合栈相关的知识,以后会从官方混合接入方案开始到闲鱼 Flutter Boost 进行介绍,下一篇文章就会是混合开发的第一篇,但愿我能不要拖更🤣

此次的内容就是这样了,若是您对本文还有任何疑问或者文章的建议,欢迎在下方评论区以及个人邮箱1652219550a@gmail.com与我联系,我会及时回复!

相关文章
相关标签/搜索