深入理解 Flutter 多线程 | 开发者说·DTalk

2019 年 7 月 23 日 谷歌开发者


本文原作者: 刘小壮,原文发布于搜狐技术产品 (sohu-tech)


导读

从18年底Flutter1.0正式版发布以来,在国内出现了爆发式增长,包括闲鱼、抖音、今日头条在内的大多数公司,相继接入了Flutter,通过Flutter进行跨平台开发。


在使用Flutter的过程中,我们也需要对其一些特性进行研究和掌握,如果只是调用API的话并不能领悟到Flutter的精髓。


本篇文章详细讲解了Flutter的多线程,由于笔者是做iOS开发的,并且将Flutter的多线程和iOS的GCD进行了对比,以帮助大家理解Flutter多线程。



事件队列

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。


和iOS应用很,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。应用程序启动后,开始执行main函数并运行main isolate


每个isolate包含一个件循环以及两个事件队列,event loop事件循环,以及event queuemicrotask queue事件队列,eventmicrotask队列有点类似iOS的source0source1

  • event queue:负责处理I/O事件、绘制事件、手势事件、接收其他isolate消息等外部事件。

  • microtask queue:可以自己向isolate内部添加事件,事件的优先级比event queue高。

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



async、await

在异步调用中有三个关键词,asyncawaitFuture,其中asyncawait需要一起使用。在Dart中可以通过asyncawait进行异步操作,async表示开启一个异步操作,也可以返回一个Future结果。如果没有返回值,则默认返回一个返回值为nullFuture


asyncawait本质上就是Dart对异步操作的一个语法糖,可以减少异步调用的嵌套调用,并且由async修饰后返回一个Future,外界可以以链式调用的方式调用。这个语法是JSES7标准中推出的,Dart的设计和JS同。


下面封装了一个网络请求的异步操作,并且将请求后的Response类型的Future返回给外界,外界可以通过await调用这个请求,并获取返回数据。从代码中可以看到,即便直接返回一个字符串,Dart也会对其进行包装并成为一个Future


 1Future<Response> dataReqeust() async {
2    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
3    Client client = Client();
4    Future<Response> response = client.get(requestURL);
5    return response;
6}
7
8Future<String> loadData() async {
9    Response response = await dataReqeust();
10    return response.body;
11

<向右滑动查看完整代码>


在代码示例中,执行到loadData方法时,会同步进入方法内部进行执行,当执行到await时就会停止async内部的执行,从而继续执行外面的代码。当await有返回后,会继续从await的位置继续执行。所以await的操作,不会影响后面代码的执行。


下面是一个代码示例,通过async开启一个异步操作,通过await等待请求或其他操作的执行,并接收返回值。当数据发生改变时,调用setState方法并更新数据源,Flutter会更新对应的Widget节点视图。


 1class _SampleAppPageState extends State<SampleAppPage{
2  List widgets = [];
3
4  @override
5  void initState() {
6    super.initState();
7    loadData();
8  }
9
10  loadData() async {
11    String dataURL = "https://jsonplaceholder.typicode.com/posts";
12    http.Response response = await http.get(dataURL);
13    setState(() {
14      widgets = json.decode(response.body);
15    });
16  }
17}

<向右滑动查看完整代码>



Future

Future就是延时操作的一个封装,可以将异步任务封装为Future对象。获取到Future对象后,最简单的方法就是用await修饰,并等待返回结果继续向下执行。正如上面async、await中讲到的,使用await修饰时需要配合async一起使用。


Dart中,和时间相关的操作基本都和Future有关,例如延时操作、异步操作等。下面是一个很简单的延时操作,通过Futuredelayed方法实现。


1loadData() {
2    // DateTime.now(),获取当前时间
3    DateTime now = DateTime.now();
4    print('request begin $now');
5    Future.delayed(Duration(seconds: 1), (){
6      now = DateTime.now();
7      print('request response $now');
8    });
9}


Dart还支持对Future的链式调用,通过追加一个或多个then方法来实现,这个特性非常实用。例如一个延时操作完成后,会调用then方法,并且可以传递一个参数给then。调用方式是链式调用,也就代表可以进行很多层的处理。这有点类似于iOS的RAC框架,链式调用进行信号处理。


1Future.delayed(Duration(seconds: 1), (){
2  int age = 18;
3  return age;
4}).then((onValue){
5  onValue++;
6  print('age $onValue');
7});



协程

如果想要了解asyncawait的原理,就要先了解协程的概念,asyncawait本质上就是协程的一种语法糖。协程,也叫作coroutine,是一种比线程更小的单元。如果从单元大小来说,基本可以理解为进程->线程->协程。


任务调度

在弄懂协程之前,首先要明白并发和并行的概念,并发指的是由系统来管理多个IO的切换,并交由CPU去处理。并行指的是多核CPU在同一时间里执行多个任务。


并发的实现由非阻塞操作+事件通知来完成,事件通知也叫做“中断”。操作过程分为两种,一种是CPU对IO进行操作,在操作完成后发起中断告诉IO操作完成。另一种是IO发起中断,告诉CPU可以进行操作。


线程本质上也是依赖于中断来进行调度的,线程还有一种叫做“阻塞式中断”,就是在执行IO操作时将线程阻塞,等待执行完成后再继续执行。但线程的消耗是很大的,并不适合大量并发操作的处理,而通过单线程并发可以进行大量并发操作。当多核CPU出现后,单个线程就无法很好的利用多核CPU的优势了,所以又引入了线程池的概念,通过线程池来管理大量线程。


协程

在程序执行过程中,离开当前的调用位置有两种方式,继续调用其他函数和return返回离开当前函数。但是执行return时,当前函数在调用栈中的局部变量、形参等状态则会被销毁。


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


async、await原理

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


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


如果用协程和iOS的dispatch_async进行对比,可以发现二者是比较相似的。从结构定义来看,协程需要将当前await的代码块相关的变量进行存储,dispatch_async也可以通过block来实现临时变量的存储能力。


我之前还在想一个问题,苹果为什么不引入协程的特性呢?后来想了一下,awaitdispatch_async都可以简单理解为异步操作,OC的线程是基于Runloop实现的,Dart本质上也是有事件循环的,而且二者都有自己的事件队列,只是队列数量和分类不同。


我觉得当执行到await时,保存当前的上下文,并将当前位置标记为待处理任务,用一个指针指向当前位置,并将待处理任务放入当前Isolate的队列中。在每个事件循环时都去询问这个任务,如果需要进行处理,就恢复上下文进行任务处理。


Promise

这里想提一下JS里的Promise语法,在iOS中会出现很多if判断或者其他的嵌套调用,而Promise可以把之前横向的嵌套调用,改成纵向链式调用。如果能把Promise引入到OC里,可以让代码看起来更简洁,直观。



isolate

isolateDart平台对线程实现方案,但和普通Thread不同的是,isolate拥有独立的内存,isolate由线程和独立内存构成。正是由于isolate线程之间的内存不共享,所以isolate线程之间并不存在资源抢夺的问题,所以也不需要锁。


通过isolute可以很好的利多核CPU,来进行大量耗时任务的处理。isolute线程之间的通信主要通过port来进行,这个port消息传递的过程是异步的。通过Dart源码也可以看出,实例化一个isolute的过程包括,实例化isolute结构体、在堆中分配线程内存、配置port等过程。


isolute看起来其实和进程比较似,之前请教阿里架构师宗心问题时,宗心也说过“isolate的整体模型我自己的理解其实更像进程,而asyncawait更像是线程”。如果对比一下isolute和进程的定义,会发现确实isolute很像是进程。


代码示例

下面是一个isolute的例子,例子中新创建了一个isolute,并且绑定了一个方法进行网络请求和数据解析的处理,并通过port将处理好的数据返回给调用方。


 1loadData() async {
2    // 通过spawn新建一个Isolute,并绑定静态方法
3    ReceivePort receivePort =ReceivePort();
4    await Isolate.spawn(dataLoader, receivePort.sendPort);
5
6    // 获取新Isolute的监听port
7    SendPort sendPort = await receivePort.first;
8    // 调用sendReceive自定义方法
9    List dataList = await sendReceive(sendPort, 
'https://jsonplaceholder.typicode.com/posts');
10    print('dataList $dataList');
11}
12
13// isolute的绑定方法
14static dataLoader(SendPort sendPort) async{
15    // 创建监听port,并将sendPort传给外界用来调用
16    ReceivePort receivePort =ReceivePort();
17    sendPort.send(receivePort.sendPort);
18
19    // 监听外界调用
20    await for (var msg in receivePort) {
21      String requestURL =msg[0];
22      SendPort callbackPort =msg[1];
23
24      Client client = Client();
25      Response response = await client.get(requestURL);
26      List dataList = json.decode(response.body);
27      // 回调返回值给调用者
28      callbackPort.send(dataList);
29    }    
30}
31
32// 创建自己的监听port,并且向新isolute发送消息
33Future sendReceive(SendPort sendPort, String url) {
34    ReceivePort receivePort =ReceivePort();
35    sendPort.send([url, receivePort.sendPort]);
36    // 接收到返回值,返回给调用者
37    return receivePort.first;
38}

<向右滑动查看完整代码>


isolate和iOS中的线程还不太一样,isolate的线程更偏底层。当生成一个isolate后,其内存是各自独立的,相互之间并不能进行访问。但isolate提供了基于port的消息机制,通过建立通信双方的sendPortreceiveport,进行相互的消息传递,在Dart中叫做消息传递。


从上面例子中可以看出,在进行isolute消息传递的过程中,本质上就是进行port的传递。将port传递给其他isolute,其他isolute通过port拿到sendPort,向调用方发送消息来进行相互的消息传递。



Embedder

正如其名,Embedder是一个嵌入层,将Flutter嵌入到各个平台上。Embedder负责范围包括原生平台插件、线程管理、事件循环等。

Embeder中存在四个Runner,四个Runner分别如下。其中每个Flutter Engine各自对应一个UI RunnerGPU RunnerIO Runner,但所有Engine共享一个Platform Runner

Runnerisolute并不是一码事,彼此相互独立。以iOS平台为例,Runner的实现就是CFRunloop,以一个事件循环的方式不断处理任务。并且Runner不只处理Engine的任务,还有Native Plugin带来的原生平台的任务。而isolute则由Dart VM进行管理,和原生平台线程并无关系。


Platform Runner

Platform Runner和iOS平台的Main Thread非常相似,在Flutter中除耗时操作外,所有任务都应该放在Platform中,Flutter中的很多API并不是线程安全的,放在其他线程中可能会导致一些bug。


但例如IO之类的耗时操作,应该放在其他线程中完成,否则会影响Platform的正常执行,甚至于被watchdog干掉。但需要注意的是,由于Embeder Runner的机制,Platform被阻塞后并不会导致页面卡顿。


不只是Flutter Engine的代码在Platform中执行,Native Plugin的任务也会派发到Platform中执行。实际上,在原生侧的代码运行在Platform Runner中,而Flutter侧的代码运行在Root Isolute中,如果在Platform中执行耗时代码,则会卡原生平台的主线程。


UI Runner

UI RunnerFlutter Engine执行Root Isolate的代码,除此之外,也处理来自Native Plugin的任务。Root Isolate为了处理自身事件,绑定了很多函数方法。程序启动时,Flutter Engine会为Root绑定UI Renner的处理函数,使Root Isolate具备提交渲染帧的能力。


Root IsolateEngine提交一次渲染帧时,Engine会等待下次vsync,当下次vsync到来时,由Root IsolateWidgets进行布局操作,并生成页面的显示信息的描述,并将信息交给Engine去处理。


于对widgets进行layout并生成layer treeUI Renner进行的,如果在UI Renner中进行大量耗时处理,会影响页面的显示,所以应该将耗时操作交给其他isolute处理,例如来自Native Plugin的事件。

GPU Runner

GPU Runner并不直接负责渲染操作,其负责GPU相关的管理和调度。当layer tree信息到来时,GPU Runner将其提交给指定的渲染平台,渲染平台是Skia配置的,不同平台可能有不同的实现。


GPU Runner相对比较独立,除了Embeder外其他线程均不可向其提交渲染信息。

IO Runner

GPU Renner中比较耗时的操作,就放在IO Runner中进行处理,例如图片读取、解压、渲染等操作。但是只有GPU Runner才能对GPU提交渲染信息,为了保证IO Runner也具备这个能力,所以IO Runner会引用GPU Runnercontext,这样就具备向GPU提交渲染信息的能力。





"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等,我们希望以此促进中国开发者彼此间的相互启发、学习和技术交流。


 点击屏末 |  | 了解更多 "开发者说·DTalk" 活动详情与参与方式




登录查看更多
0

相关内容

Google 发布的面向结构化 web 应用的开语言。 dartlang.org
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
234+阅读 · 2020年5月21日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
163+阅读 · 2019年10月28日
重磅!Flutter 2019年最新进展和未来展望
前端之巅
4+阅读 · 2019年6月20日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
免费!Google Colab现已支持英伟达T4 GPU
新智元
5+阅读 · 2019年4月25日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
深度学习 | 免费使用Google Colab的GPU云计算平台
沈浩老师
12+阅读 · 2018年2月4日
深度学习中的「卷积层」如何深入理解?
深度学习世界
6+阅读 · 2017年11月30日
Directions for Explainable Knowledge-Enabled Systems
Arxiv
26+阅读 · 2020年3月17日
Music Transformer
Arxiv
5+阅读 · 2018年12月12日
Arxiv
6+阅读 · 2018年5月18日
Arxiv
3+阅读 · 2018年3月2日
VIP会员
相关VIP内容
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
234+阅读 · 2020年5月21日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
163+阅读 · 2019年10月28日
相关资讯
重磅!Flutter 2019年最新进展和未来展望
前端之巅
4+阅读 · 2019年6月20日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
免费!Google Colab现已支持英伟达T4 GPU
新智元
5+阅读 · 2019年4月25日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
深度学习 | 免费使用Google Colab的GPU云计算平台
沈浩老师
12+阅读 · 2018年2月4日
深度学习中的「卷积层」如何深入理解?
深度学习世界
6+阅读 · 2017年11月30日
Top
微信扫码咨询专知VIP会员