Flutter线程

线程简述:

        在开发过程中,我们经常会用到例如网络请求,文件读取等一系列耗时操作,而我们的主线程要完成绘制ui,处理用户响应等一系列操作,假如让我们的主线程去等待耗时操作结果那么后续绘制ui,处理用户响应的操作就会被阻塞,显然是不现实的。那么我该如何处理耗时操作。

线程处理:

针对线程耗时处理操作,不同语言各有不同的处理方式与优缺点。

    • 多线程:在Java、C++中就是开启一个新的线程,将耗时操作放在新的线程里面处理,再通过线程间通信的方式,将拿到的数据传给主线程处理。
    • 单线程:在比如 JavaScript、Dart 都是基于单线程加事件循环来完成耗时操作的处理。

单线程耗时操作方式:

    • 阻塞式调用: 调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行
    • 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。

而Dart 的异步操作就是利用非阻塞式调用实现的。

Dart中的异步操作:

        Dart中的异步操作主要使用Future以及async、await,async 和 await 是要一起使用的,这属于是协程的一个语法糖。

  • Future 延时操作的一个封装,可以将异步任务封装为Future对象,我们通常通过then()来处理返回的结果
  • async 用于标明函数是一个异步函数,其返回值类型是Future类型
  • await 用来等待耗时操作的返回结果,这个操作会阻塞到后面的任务

什么是协程

        协程分为无线协程和有线协程,无线协程在离开当前调用位置时,会将当前变量放在堆区,当再次回到当前位置时,还会继续从堆区中获取到变量。所以,一般在执行当前函数时就会将变量直接分配到堆区,而async、await就属于无线协程的一种。有线协程则会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出调用。

async、await原理

        拿async、await为例,协程在执行时,执行到async则表示进入一个协程,会同步执行async的代码块。async的代码本质上是一个函数,并且有自己的上下文环境。当执行到该函数的await行的代码时,表示有任务需要等待,CPU 则去调度执行其他 IO,也就是后面的代码或其他协程代码。过一段时间 CPU 就会轮循一次,看某个协程是否任务已经处理完成,有返回结果可以被继续执行,如果可以被继续执行的话,则会沿着上次离开时指针指向的位置继续执行,也就是await标志的位置。

        由于并没有开启新的线程,只是进行 IO 中断改变 CPU 调度,所以网络请求这样的异步操作可以使用async、await,但如果是执行大量耗时同步操作的话,应该使用isolate开辟新的线程去执行。

模拟同步与异步操作:

@override
void initState() {
  // TODO: implement initState
  super.initState();
  imitateMethod();
  print("1");
}
void imitateMethod() {
  print("2");
  Future.delayed(Duration(seconds:1),(){
    print("3");
  });
  print("4");
}

结果如下:并没有因为3有耗时操作Future而影响下一步代码执行。

我们现在改造一下加上async 与 await后的

@override
void initState() {
  // TODO: implement initState
  super.initState();
  imitateMethod();
  print("1");
}
void imitateMethod() async{
  print("2");
  await Future.delayed(Duration(seconds:1),(){
    print("3");
  });
  print("4");
}

输出结果如下3 等待了耗时操作完成之后才执行。使用 async 来标明 imitateMethod 这个函数是一个异步函数,await 用于等待请求返回的结果,此时会阻塞掉后面的代码,只有当请求结束后面的代码才会执行。

多Future的执行顺序是什么样呢?

@override
void initState() {
  // TODO: implement initState
  super.initState();
  imitateMethod();
  print("1");
}

void imitateMethod() {
  Future(() {
    print("2");
  });
  Future(() {
    print("3");
  });
  Future(() {
    print("4");
  });
}

可以看到结果为顺序执行(关于这块我会在后面的文章讲述)

什么是事件循环:

在Dart中有一个叫做isolate的,在应用启动后运行main函数并绑定一个Root isolate。每个 isolate 里面都会有一个事件循环以及两个事件队列随着isolate创建自动开启,event loop事件循环,以及event queue和microtask queue事件队列,event 和 microtask 队列有点类似 iOS 的NSRunLoop 和Android的Looper。

    • event queue:负责处理I/O事件、绘制事件、手势事件、接收其他 isolate 消息等外部事件。
    • microtask queue:可以自己向 isolate 内部添加事件,事件的优先级比 event queue高

下图为Dart的任务执行顺序:

在isolate执行中,会先执行microtask(微任务),当微任务队列中没有事件后,才会处理 event(事件)队列中的事件,并按照这个顺序反复执行。但需要注意的是,当执行微任务事件时,会阻塞事件队列的事件执行,这样就会导致渲染、手势响应等 event 事件响应延时。为了保证渲染和手势响应,应该尽量将耗时操作放在事件队列中。

下面我们使用代码来证明这一点

void testTaskQueue() {
  print("开始执行");
  Timer.run(() {
    print("事件队列1 立即执行");
  });
  Future.delayed(Duration(seconds: 2), () {
    print("事件队列2 延迟2秒执行");
  });
  scheduleMicrotask(() {
    print("微任务队列1 task 立即执行");
  });
  print("结束执行");
}

结果为:

可以看到微任务队列的执行优先级是比事件队列快的。

在页面中做耗时比较大的运算时,即使使用了 async、await 异步处理,UI页面的动画还是会卡顿,因为还是在这个UI线程中做运算,异步只是你可以先做其他,等我这边有结果再返回,但是仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。所以这个时候就需要创建新的线程来执行耗时操作解决这个问题。

Dart isolate

前言:

官方的定义中 isolate是一个隔离Dart执行上下文,dart代码都运行在isolate中,每个isolate是由独立的线程和内存构成,isolate之间拥有独立的内存不共享内存,通信方式为只能通过Port,且消息传递总是异步的,正是因为Dart中没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不用担心死锁的问题。

在Dart2.15后Google考虑到我们可能会使用到并发运行,于是基于isolate设计了isolate组,在Isolate 组中的 isolate 共享各种内部数据结构,这些数据结构则表示正在运行的程序。这使得组中的单个 isolate 变得更加轻便。如今,因为不需要初始化程序结构,在现有 isolate 组中启动额外的 isolate 比之前快 100 多倍,并且产生的 isolate 所消耗的内存减少了 10 至 100 倍。

isolate与普通线程的区别

isolate准确的说应该叫做协程,协程最大的优势就是它具有极高的执行效率,因为携程中子程序的调用不需要线程的切换,所以对于线程数量越大的程序来说协程的优势就越明显

isolate的使用:

void testIsolateMethod() async{
  print("start");
  //创建ReceivePort对象
  ReceivePort receivePort = ReceivePort();
  //创建Isolate对象
  Isolate spawn = await Isolate.spawn(printLog, receivePort.sendPort);
  receivePort.listen((message) {
    print("result === $message");
    //关闭监听
    receivePort.close();
    //销毁线程
    spawn.kill();
  });
  print("end");
}
  static printLog(SendPort sendPort){
    sendPort.send("来自子线程的信息");
  }

打印结果:

isloate的多线程

void testIsolatesThread(){
 	print("start");
  Isolate.spawn(recive, 1);
  Isolate.spawn(recive, 2);
  Isolate.spawn(recive, 3);
  Isolate.spawn(recive, 4);
  Isolate.spawn(recive, 5);
  Isolate.spawn(recive, 6);
  Isolate.spawn(recive, 7);
  Isolate.spawn(recive, 8);
  Isolate.spawn(recive, 9);
  Isolate.spawn(recive, 10);
 	print("end");
}
static  recive(int index){
  print("第$index个");
}

我们看下面的结果可以得知isolate的执行是不按创建的顺序走的

在通过下两图我们可以看到开启十个isolate的内存前后对比。

开启前:

开启后:

正常我们还是要注意isolate的创建与销毁。少开启多个线程。

Flutter 架构与isolate的关联

前言

随着对Flutter的使用,我越来越对Flutter的线程存在着一些疑问,例如Flutter的体系结构是什么,为什么Dart默认是单线程任务处理那么单线程下耗时操作该怎么处理,如何使用。

Flutter 体系结构与线程的关联

介绍一下Flutter 的主要三个核心模块

    • Framework:基于 Dart 语言构建的 framework,包括了动画以及各种组件
    • Engine:基于 C/C++ 构建的引擎,包括了 Skia 和 DartVM, 以及在不同平台实现的 shell 层,Engine 通过封装好的 Embedder API 去调用不同平台的能力
    • Embedder:嵌入器,将 Flutter 嵌入到各个平台上。Embedder 负责范围包括原生平台插件、线程管理、事件循环等。

Flutter Engine 自己并不创建和管理线程,线程的创建和管理全都由 Embedder 负责,Embedder 提供 Task Runner 给 Engine,除了 Engine 使用的 Runner 之外,DartVM 还有自己的线程池,但这些都是 Engine 和 Embedder 无法访问的。DartVM 中的线程池主要是用来实现语言特性比如异步等等

Flutter Engine要求Embeder提供四个Task Runner,Embeder指的是将引擎移植到平台的中间层代码。这四个主要的Task Runner包括:

Platform Runner Thread:

Flutter Engine的主Task Runner,类似于Android或iOS的Main Thread()。但是他们之间也是有区别的。

一般来说,一个Flutter 应用启动的时候会去创建一个Engine的实例,Engine创建实例的时候也会去创建一个线程供Platform Runner使用

跟Flutter Engine的所有交互(接口调用)必须在Platform Thread 进行,否则会导致无法预期的异常,这跟Android和iOS的ui相关操作必须放在主线程类似。且Platlform Runner所在的Thread不仅跟Engine交互,同时他也处理来自平台的消息,这样处理比较方便因为几乎所有引擎的调用都只有在Platform Thread进行才能是安全的,Native Plugins不必要做额外的线程操作就可以保证操作能够在Platform Thread进行。如果Plugin自己启动了额外的线程,那么它需要负责将返回结果派发回Platform Thread以便Dart能够安全地处理。但Flutter Engine中有很多都是非线程安全的。一旦引擎正常启动运行起来,确保安全,对于FlutterEngine 的调用都需要保证在Platform Thread中进行。

阻塞Platform Runner不会直接导致Flutter应用卡顿(跟Android、iOS中的主线程不同),但不建议在这个Thread中执行繁重的耗时操作(额外的线程做),长时间卡住的Platform Thread有可能被系统Watchdog强行杀死应用。

UI Runner Thread:

UI Task Runner用于执行Dart root isolate代码(Dart VM里面的根线程)。Root isolate比较特殊是运行所有dart代码的地方同时也绑定了不少Flutter需要的函数方法,以便进行渲染相关操作。对于每一帧,引擎要做的事情有:

    • Root isolate通知Flutter Engine有帧需要渲染。
    • Flutter Engine通知平台,需要在下一个vsync的时候得到通知。
    • 平台等待下一个vsync
    • 对创建的对象和Widgets进行Layout并生成一个Layer Tree,这个Tree马上被提交给Flutter Engine。当前阶段没有进行任何光栅化,这个步骤仅是生成了对需要绘制内容的描述。
    • 创建或者更新Tree,这个Tree包含了用于屏幕上显示Widgets的语义信息。这个东西主要用于平台相关的辅助Accessibility元素的配置和渲染。

生成Layer Tree 的过程

除了渲染相关逻辑之外Root Isolate还是处理来自Native Plugins的消息,Timers,Microtasks和异步IO等操作。Root Isolate负责创建管理的Layer Tree最终决定绘制到屏幕上的内容。这个线程的过载会直接导致卡顿掉帧。

GPU Runner Thread (又被称为Raster Task Runner):

GPU Runner虽然叫GPU,但他并不运行在GPU里面,他只是为GPU进行栅格化的,用于执行设备GPU的指令他的线程所以又被称为Raster Runner。UI Task Runner创建的Layer Tree是跨平台的,它不关心到底由谁来完成绘制。GPU Task Runner负责将Layer Tree提供的信息转化为平台可执行的GPU指令。GPU Task Runner同时负责绘制所需要的GPU资源的管理。资源主要包括平台Framebuffer,Surface,Texture和Buffers等。

基于Layer Tree的处理时长和GPU帧显示到屏幕的耗时,GPU Task Runner可能会延迟下一帧在UI Task Runner的调度。一般来说UI Runner和GPU Runner跑在不同的线程。存在这种可能,UI Runner在已经准备好了下一帧的情况下,GPU Runner却还正在向GPU提交上一帧。这种延迟调度机制确保不让UI Runner分配过多的任务给GPU Runner。

前面我们提到GPU Runner可以导致UI Runner的帧调度的延迟,GPU Runner的过载会导致Flutter应用的卡顿。一般来说UI Runner和GPU Runner跑在不同的线程。GPU Runner会根据目前帧执行的进度去向UI Runner要求下一帧的数据,在任务繁重的时候可能会告诉UI Runner延迟任务。这种调度机制确保GPU Runner不至于过载,同时也避免了UI Runner不必要的消耗。建议为每一个Engine实例都新建一个专用的GPU Runner线程。

IO Task Runner:

前面讨论的几个Runner对于执行流畅度有比较高的要求。Platform Runner过载可能导致系统WatchDog强杀,UI和GPU Runner过载则可能导致Flutter应用的卡顿。但是GPU线程的一些必要操作,例如IO,放到哪里执行呢?答案正是IO Runner。

IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备。在Texture的准备过程中,IO Runner首先要读取压缩的图片二进制数据(比如PNG,JPEG),将其解压转换成GPU能够处理的格式然后将数据上传到GPU。这些复杂操作如果跑在GPU线程的话会导致Flutter应用UI卡顿。但是只有GPU Runner能够访问GPU,所以IO Runner模块在引擎启动的时候配置了一个特殊的Context,这个Context跟GPU Runner使用的Context在同一个ShareGroup。事实上图片数据的读取和解压是可以放到一个线程池里面去做的,但是这个Context的访问只能在特定线程才能保证安全。这也是为什么需要有一个专门的Runner来处理IO任务的原因。获取诸如ui.Image这样的资源只有通过async call,当这个调用发生的时候Flutter Framework告诉IO Runner进行刚刚提到的那些图片异步操作。这样GPU Runner可以使用IO Runner准备好的图片数据而不用进行额外的操作。

用户操作,无论是Dart Code还是Native Plugins都是没有办法直接访问IO Runner。尽管Embeder可以将一些一般复杂任务调度到IO Runner,这不会直接导致Flutter应用卡顿,但是可能会导致图片和其它一些资源加载的延迟间接影响性能。所以建议为IO Runner创建一个专用的线程。

参考链接:闲鱼技术谷歌开发者

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值