Flutter学习笔记-多线程
Flutter的优点:
-
Flutter在Rlease模式下直接将Dart编译成本地机器码,避免了代码解释运行的性能消耗。
-
Dart本身针对高频率循环刷新(如屏幕每秒60帧)在内存层面进行了优化,使得Dart运行时在屏幕绘制实现如鱼得水。
-
Flutter实现了自己的图形绘制避免了Native桥接。
Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。
Flutter的体系结构:
事件队列
和其他系统一样,在Dart
的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate
。应用程序启动后,开始执行main函数并运行main isolate
。
每个isolate包含一个事件循环以及两个事件队列,event loop
事件循环,以及event queue
和microtask queue
事件队列,event和microtask队列有点类似iOS的source0
和source1
。
source1 :苹果创bai建用来接受系统发出事件,当du手机发生一个触摸,摇晃或锁屏zhi等系统dao,这时候系统会发送一个事件到app进程(进程通信),这也就是为什么叫基于port传递source1的原因,port就是进程端口嘛,该事件可以**进程里线程的runloop,比如你点击一下app的按钮或屏幕,runloop就醒过来处理触摸事件,你可以做个实验,在主线程的runloop中添加一个CFRunLoopObserverRef,用switch输出runloop6个状态,这时候你每点击一次屏幕,他就会输出Runloop六个状态,然后进入休眠。
source0 :执行performSelectors方法,假如你在主线程performSelectors一个任务到子线程,这时候就是在代码中发送事件到子线程的runloop,这时候如果子线程开启了runloop就会执行该任务,注意该performSelector方法只有在你子线程开启runloop才能执行,如果你没有在子线程中开启runloop,那么该操作会无法执行并崩溃。
-
event queue:负责处理I/O事件、绘制事件、手势事件、接收其他isolate消息等外部事件。
-
microtask queue:可以自己向isolate内部添加事件,事件的优先级比event queue高。
这两个队列也是有优先级的,当isolate
开始执行后,会先处理microtask
的事件,当microtask
队列中没有事件后,才会处理event
队列中的事件,并按照这个顺序反复执行。但需要注意的是,当执行microtask
事件时,会阻塞event
队列的事件执行,这样就会导致渲染、手势响应等event
事件响应延时。为了保证渲染和手势响应,应该尽量将耗时操作放在event
队列中。
异步调用 - async、await
在异步调用中有三个关键词,async
、await
、Future
,其中async和await需要一起使用。在Dart中可以通过async和await进行异步操作,async表示开启一个异步操作,也可以返回一个Future结果。如果没有返回值,则默认返回一个返回值为null的Future。
async、await本质上就是Dart对异步操作的一个语法糖,可以减少异步调用的嵌套调用,并且由async修饰后返回一个Future,外界可以以链式调用的方式调用。这个语法是JS的ES7标准中推出的,Dart的设计和JS相同。
使用示例:
网络请求的方法
Future<HttpClientResponse> dataRequest() async {
//创建一个HttpClient
HttpClient httpClient = HttpClient();
//打开Http连接
HttpClientRequest request = await httpClient.getUrl(Uri.parse("http://www.baidu.com"));
HttpClientResponse response = await request.close();
return response;
}
Future<String> loadData() async {
HttpClientResponse response = await dataRequest();
return response.transform(utf8.decoder).join();
}
使用:
@override
void initState() {
super.initState();
loadData().then((value) {
setState(() {
_content = value;
});
});
}
在代码示例中,执行到loadData
方法时,会同步进入方法内部进行执行,当执行到await
时就会停止async
内部的执行,从而继续执行外面的代码。当await
有返回后,会继续从await
的位置继续执行。所以await
的操作,不会影响后面代码的执行。
Future
Future
就是延时操作的一个封装,可以将异步任务封装为Future
对象。获取到Future
对象后,最简单的方法就是用await
修饰,并等待返回结果继续向下执行。正如上面async
、await
中讲到的,使用await
修饰时需要配合async
一起使用。
在Dart
中,和时间相关的操作基本都和Future
有关,例如延时操作、异步操作等。
Future.delayed(Duration(seconds: 2),(){
setState(() {
_isLoading = false;
_isRequestSuccess = false;
});
finishRefresh();
});
Dart
还支持对Future
的链式调用,通过追加一个或多个then
方法来实现,这个特性非常实用。例如一个延时操作完成后,会调用then
方法,并且可以传递一个参数给then
。调用方式是链式调用,也就代表可以进行很多层的处理。这有点类似于iOS的RAC
框架,链式调用进行信号处理。
Future.delayed(Duration(seconds: 1), (){
int age = 18;
return age;
}).then((onValue){
onValue++;
print('age $onValue');
});
协程
如果想要了解async、await的原理,就要先了解协程的概念,async、await本质上就是协程的一种语法糖。协程,也叫作coroutine,是一种比线程更小的单元。如果从单元大小来说,基本可以理解为进程->线程->协程。
任务调度
在弄懂协程之前,首先要明白并发和并行的概念,并发指的是由系统来管理多个IO的切换,并交由CPU去处理。并行指的是多核CPU在同一时间里执行多个任务。
并发的实现由非阻塞操作+事件通知来完成,事件通知也叫做“中断”。操作过程分为两种,一种是CPU对IO进行操作,在操作完成后发起中断告诉IO操作完成。另一种是IO发起中断,告诉CPU可以进行操作。
线程本质上也是依赖于中断来进行调度的,线程还有一种叫做“阻塞式中断”,就是在执行IO操作时将线程阻塞,等待执行完成后再继续执行。但线程的消耗是很大的,并不适合大量并发操作的处理,而通过单线程并发可以进行大量并发操作。当多核CPU出现后,单个线程就无法很好的利用多核CPU的优势了,所以又引入了线程池的概念,通过线程池来管理大量线程。
协程
在程序执行过程中,离开当前的调用位置有两种方式,继续调用其他函数和return返回离开当前函数。但是执行return时,当前函数在调用栈中的局部变量、形参等状态则会被销毁。
协程分为无线协程和有线协程,无线协程在离开当前调用位置时,会将当前变量放在堆区,当再次回到当前位置时,还会继续从堆区中获取到变量。所以,一般在执行当前函数时就会将变量直接分配到堆区,而async、await就属于无线协程的一种。有线协程则会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出调用。
async、await原理
以async、await为例,协程在执行时,执行到async则表示进入一个协程,会同步执行async的代码块。async的代码块本质上也相当于一个函数,并且有自己的上下文环境。当执行到await时,则表示有任务需要等待,CPU则去调度执行其他IO,也就是后面的代码或其他协程代码。过一段时间CPU就会轮训一次,看某个协程是否任务已经处理完成,有返回结果可以被继续执行,如果可以被继续执行的话,则会沿着上次离开时指针指向的位置继续执行,也就是await标志的位置。
由于并没有开启新的线程,只是进行IO中断改变CPU调度,所以网络请求这样的异步操作可以使用async、await,但如果是执行大量耗时同步操作的话,应该使用isolate开辟新的线程去执行。
如果用协程和iOS的dispatch_async进行对比,可以发现二者是比较相似的。从结构定义来看,协程需要将当前await的代码块相关的变量进行存储,dispatch_async也可以通过block来实现临时变量的存储能力。
我之前还在想一个问题,苹果为什么不引入协程的特性呢?后来想了一下,await和dispatch_async都可以简单理解为异步操作,OC的线程是基于Runloop实现的,Dart本质上也是有事件循环的,而且二者都有自己的事件队列,只是队列数量和分类不同。
我觉得当执行到await时,保存当前的上下文,并将当前位置标记为待处理任务,用一个指针指向当前位置,并将待处理任务放入当前isolate的队列中。在每个事件循环时都去询问这个任务,如果需要进行处理,就恢复上下文进行任务处理。
Flutter 线程模型
Flutter Engine自己不创建管理线程。Flutter Engine线程的创建和管理是由embedder负责的。Embeder指的是将引擎移植到平台的中间层代码。
Flutter Engine要求Embeder提供四个Task Runner。尽管Flutter Engine不在乎Runner具体跑在哪个线程,但是它需要线程配置在整一个生命周期里面保持稳定。也就是说一个Runner最好始终保持在同一线程运行。这四个主要的Task Runner包括:
Platform Task Runner
Flutter Engine的主Task Runner,运行Platform Task Runner的线程可以理解为是主线程。类似于Android Main Thread或者iOS的Main Thread。但是我们要注意Platform Task Runner和iOS之类的主线程还是有区别的。
对于Flutter Engine来说Platform Runner所在的线程跟其它线程并没有实质上的区别,只不过我们人为赋予它特定的含义便于理解区分。实际上我们可以同时启动多个Engine实例,每个Engine对应一个Platform Runner,每个Runner跑在各自的线程里。
跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟iOS UI相关的操作都必须在主线程进行相类似。需要注意的是在Flutter Engine中有很多模块都是非线程安全的。一旦引擎正常启动运行起来,所有引擎API调用都将在Platform Thread里发生。
Platform Runner所在的Thread不仅仅处理与Engine交互,它还处理来自平台的消息。这样的处理比较方便的,因为几乎所有引擎的调用都只有在Platform Thread进行才能是安全的,Native Plugins不必要做额外的线程操作就可以保证操作能够在Platform Thread进行。如果Plugin自己启动了额外的线程,那么它需要负责将返回结果派发回Platform Thread以便Dart能够安全地处理。规则很简单,对于Flutter Engine的接口调用都需保证在Platform Thread进行。
需要注意的是,阻塞Platform Thread不会直接导致Flutter应用的卡顿(跟iOS android主线程不同)。尽管如此,平台对Platform Thread还是有强制执行限制。所以建议复杂计算逻辑操作不要放在Platform Thread而是放在其它线程(不包括我们现在讨论的这个四个线程)。其他线程处理完毕后将结果转发回Platform Thread。长时间卡住Platform Thread应用有可能会被系统Watchdog强行杀死。
UI Task Runner Thread(Dart Runner)
UI Task Runner被Flutter Engine用于执行Dart root isolate代码(isolate我们后面会讲到,姑且先简单理解为Dart VM里面的线程)。Root isolate比较特殊,它绑定了不少Flutter需要的函数方法。Root isolate运行应用的main code。引擎启动的时候为其增加了必要的绑定,使其具备调度提交渲染帧的能力。对于每一帧,引擎要做的事情有: - Root isolate通知Flutter Engine有帧需要渲染。 - Flutter Engine通知平台,需要在下一个vsync的时候得到通知。 - 平台等待下一个vsync - 对创建的对象和Widgets进行Layout并生成一个Layer Tree,这个Tree马上被提交给Flutter Engine。当前阶段没有进行任何光栅化,这个步骤仅是生成了对需要绘制内容的描述。 - 创建或者更新Tree,这个Tree包含了用于屏幕上显示Widgets的语义信息。这个东西主要用于平台相关的辅助Accessibility元素的配置和渲染。
除了渲染相关逻辑之外Root Isolate还是处理来自Native Plugins的消息响应,Timers,Microtasks和异步IO。 我们看到Root Isolate负责创建管理的Layer Tree最终决定什么内容要绘制到屏幕上。因此这个线程的过载会直接导致卡顿掉帧。 如果确实有无法避免的繁重计算,建议将其放到独立的Isolate去执行,比如使用compute关键字或者放到非Root Isolate,这样可以避免应用UI卡顿。但是需要注意的是非Root Isolate缺少Flutter引擎需要的一些函数绑定,你无法在这个Isolate直接与Flutter Engine交互。所以只在需要大量计算的时候采用独立Isolate。
GPU Task Runner
GPU Task Runner被用于执行设备GPU的相关调用。UI Task Runner创建的Layer Tree信息是平台不相关,也就是说Layer Tree提供了绘制所需要的信息,具体如何实现绘制取决于具体平台和方式,可以是OpenGL,Vulkan,软件绘制或者其他Skia配置的绘图实现。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应用的卡顿。一般来说用户没有机会向GPU Runner直接提交任务,因为平台和Dart代码都无法跑进GPU Runner。但是Embeder还是可以向GPU 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创建一个专用的线程。
Dart isolate机制
isolate是Dart平台对线程的实现方案,但和普通Thread不同的是,isolate拥有独立的内存,isolate由线程和独立内存构成。正是由于isolate线程之间的内存不共享,所以isolate线程之间并不存在资源抢夺的问题,所以也不需要锁。
通过isolate可以很好的利用多核CPU,来进行大量耗时任务的处理。isolate线程之间的通信主要通过port来进行,这个port消息传递的过程是异步的。通过Dart源码也可以看出,实例化一个isolate的过程包括,实例化isolate结构体、在堆中分配线程内存、配置port等过程。
isolate看起来其实和进程比较相似, 而async、await更像是线程。如果对比一下isolate和进程的定义,会发现确实isolate很像是进程。
- isolate定义
isolate是Dart对actor并发模式的实现。运行中的Dart程序由一个或多个actor组成,这些actor也就是Dart概念里面的isolate。isolate是有自己的内存和单线程控制的运行实体。isolate本身的意思是“隔离”,因为isolate之间的内存在逻辑上是隔离的。isolate中的代码是按顺序执行的,任何Dart程序的并发都是运行多个isolate的结果。因为Dart没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不用担心死锁的问题。
- isolate之间的通信
由于isolate之间没有共享内存,所以他们之间的通信唯一方式只能是通过Port进行,而且Dart中的消息传递总是异步的。
- isolate与普通线程的区别
我们可以看到isolate神似Thread,但实际上两者有本质的区别。操作系统内内的线程之间是可以有共享内存的而isolate没有,这是最为关键的区别。
- Dart本身抽象了isolate和thread,实际上底层还是使用操作系统的提供的OSThread。
代码示例:
新建一个线程(isolate)处理网络请求。
otherLoadData() async {
//通过spawn新建一个isolate,并绑定静态方法
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
//获取新isolate的监听port
SendPort sendPort = await receivePort.first;
//调用sendReceive自定义方法
String result = await sendReceive(sendPort, 'http://www.baidu.com');
setState(() {
_content = result;
});
}
//isolate的绑定方法
static dataLoader(SendPort sendPort) async {
//创建监听port,并将sendPort传给外界用来调用
ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
//监听外界调用
await for (var msg in receivePort) {
String requestURL = msg[0];
SendPort callbackPort = msg[1];
HttpClient client = HttpClient();
HttpClientRequest request = await client.getUrl(Uri.parse(requestURL));
HttpClientResponse response = await request.close();
String dataStr = await response.transform(utf8.decoder).join();
//回调返回值给调用者
callbackPort.send(dataStr);
}
}
// 创建自己的监听port,并且向新isolate发送消息
Future sendReceive(SendPort sendPort, String url) {
ReceivePort receivePort = ReceivePort();
sendPort.send([url, receivePort.sendPort]);
//接收到返回值,返回给调用者
return receivePort.first;
}
@override
void initState() {
super.initState();
otherLoadData();
}
Flutter Engine Runners与Dart Isolate
既然Flutter Engine有自己的Runner,那为何还要Dart的Isolate呢,他们之间又是什么关系呢?
那我们还要从Runner具体的实现说起,Runner是一个抽象概念,我们可以往Runner里面提交任务,任务被Runner放到它所在的线程去执行,这跟iOS GCD的执行队列很像。我们查看iOS Runner的实现实际上里面是一个loop,这个loop就是CFRunloop,在iOS平台上Runner具体实现就是CFRunloop。被提交的任务被放到CFRunloop去执行。
Dart的Isolate是Dart虚拟机自己管理的,Flutter Engine无法直接访问。Root Isolate通过Dart的C++调用能力把UI渲染相关的任务提交到UI Runner执行这样就可以跟Flutter Engine相关模块进行交互,Flutter UI相关的任务也被提交到UI Runner也可以相应的给Isolate一些事件通知,UI Runner同时也处理来自App方面Native Plugin的任务。
所以简单来说Dart isolate跟Flutter Runner是相互独立的,他们通过任务调度机制相互协作。
综合代码:
import 'dart:io';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:isolate';
import 'dart:async';
class HttpReqPage extends StatefulWidget {
@override
_HttpReqPageState createState() => _HttpReqPageState();
}
class _HttpReqPageState extends State<HttpReqPage> {
String _content;
Future<HttpClientResponse> dataRequest() async {
//创建一个HttpClient
HttpClient httpClient = HttpClient();
//打开Http连接
HttpClientRequest request = await httpClient.getUrl(Uri.parse("http://www.baidu.com"));
HttpClientResponse response = await request.close();
return response;
}
Future<String> loadData() async {
HttpClientResponse response = await dataRequest();
return response.transform(utf8.decoder).join();
}
otherLoadData() async {
//通过spawn新建一个isolate,并绑定静态方法
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
//获取新isolate的监听port
SendPort sendPort = await receivePort.first;
//调用sendReceive自定义方法
String result = await sendReceive(sendPort, 'http://www.baidu.com');
setState(() {
_content = result;
});
}
//isolate的绑定方法
static dataLoader(SendPort sendPort) async {
//创建监听port,并将sendPort传给外界用来调用
ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
//监听外界调用
await for (var msg in receivePort) {
String requestURL = msg[0];
SendPort callbackPort = msg[1];
HttpClient client = HttpClient();
HttpClientRequest request = await client.getUrl(Uri.parse(requestURL));
HttpClientResponse response = await request.close();
String dataStr = await response.transform(utf8.decoder).join();
//回调返回值给调用者
callbackPort.send(dataStr);
}
}
// 创建自己的监听port,并且向新isolate发送消息
Future sendReceive(SendPort sendPort, String url) {
ReceivePort receivePort = ReceivePort();
sendPort.send([url, receivePort.sendPort]);
//接收到返回值,返回给调用者
return receivePort.first;
}
@override
void initState() {
super.initState();
// loadData().then((value) {
// setState(() {
// _content = value;
// });
// });
otherLoadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("多线程demo"),
),
body: SingleChildScrollView(
child: Text(_content??"", style: TextStyle(
fontSize: 15,
color: Colors.black
),),
),
);
}
}
效果: