Dart进阶记录

919 阅读10分钟

前言

Dart基础的一些相关记录

参考资料

Dart线程模型及异常捕获

Flutter从启动到显示

Dart语言异步编程之Isolate

聊聊Dart多线程

Flutter Engine线程管理与Dart Isolate机制

Dart语言异步编程之Future

Dart学习笔记(31):Future和异常处理

Dart异步支持之Stream

目录

一、Dart单线程模型

Dart是单线程模型,运行的流程如下图

和Android的Loop有点像,简单来说,Dart在单线程中是以消息循环机制来运行的,包含两个任务队列,一个是微任务队列 Microtask queue ,另一个是 事件队列 event queue。从图中可以看出微任务队列的执行优先级高于事件队列

在入口main函数执行完后,开启了消息循环,会按先进先出的顺序逐个执行微任务中的任务,当所有微任务队列执行完后便开始执行事件队列的任务。

在Dart中,所有外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部并且微任务非常少。为什么微任务很少,从图中也可以看出来,微任务优先级高,任务太多,执行总事件和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是会很卡。我们可以通过Future.microtask()来向微任务队列插入一个任务。

注意,当在事件循环过程中,某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,不会影响道其它任务执行。至于异常捕获,可以查看 Dart线程模型及异常捕获

Flutter启动到显示分析序列图 详细分析见 Flutter从启动到显示

二、Dart多任务并行

Dart是单线程多任务的,(为什么设计成单线程的? 从APP角度来说,APP运行中,确实多线程方式显得多余,因为大多时候处于空闲状态,并不需要执行太多密集型任务)不存在多线程,那么Dart是如何进行多任务并行的呢? Flutter的多线程主要是依赖于Dart的并发编程、异步和事件驱动机制。

首先先了解一下Isolate, 直译就是隔离的意思,在Dart中,它是类似于线程Thread但不共享内存的独立运行的worker,是一个独立的Dart程序执行环境,默认环境是一个main Isolate。(Isolate代表一个Isolate对象,isolate代表一个独立的Dart代码执行环境)

具体使用交互可查看 Dart语言异步编程之Isolate

isolate之间的交互如下图

在Dart中,

一个Isolate对象其实就是一个isolate执行环境的引用,

一般来说我们都是通过当前的isolate去控制其他的isolate完成彼此之间的交互,

而当我们想要创建一个新的Isolate可以使用Isolate.spawn方法获取返回的一个新的isolate对象,

两个isolate之间使用SendPort相互发送消息,

而isolate中也存在了一个与之对应的ReceivePort接受消息用来处理,

但是我们需要注意的是,ReceivePort和SendPort在每个isolate都有一对,只有同一个isolate中的ReceivePort才能接受到当前类的SendPort发送的消息并且处理。

具体使用可以查看 Dart语言异步编程之Isolate 聊聊Dart多线程

通过上述操作来实现Dart中的多线程

那么你接下来可能会问了,Flutter是用Dart语言的,那Flutter APP中,不会也是一个单线程的吧?或者底层使用Isolate构造了多线程?Flutter的线程和Dart的isolate有啥关系?

请查看下图

对于UI Runner和Platform Runner等的详细介绍可以查看 Flutter Engine线程管理与Dart Isolate机制

这上面的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是相互独立的,他们通过任务调度机制相互协作。

三、Dart中的Future关键字

在Java并发编程中,经常会使用Future来处理异步或者延迟任务操作,而在Dart多任务编程中,同样也会用到Future,在Dart的每一个isolate中,执行的优先级为Main > MiicroTask > EventQueu 。(Flutter中,异步更新UI通常就使用FutureBuffer或者StreamBuilder)

Future类是对未来结果的一个代理,它返回的并不是被调用的任务的返回值,它会将任务添加到事件队列中,因此计算过程中产生的异常并不在当前代码块中,以致try-catch并不能捕获Future中的异常。Flutter是如何捕获异常的?可以查看 Dart线程模型及异常捕获

void  myTask(){
    print("this is my task");
}
​
void  main() {
    Future fut = new  Future(myTask);
}

如上述代码,Future类实例fut并不是函数myTask的返回值,它只是代理了myTask函数,封装了该任务执行的状态。

Flutter中几种创建Future的方法,具体使用可以见 官网Dart语言异步编程之Future 或者 Dart学习笔记(31):Future和异常处理

Future()
Future.microtask()
Future.sync()  同步方法,立马执行
Future.value()
Future.delayed()
Future.error()
Future.wait()
关于注册回调,当Future中的任务完成后,我们往往需要一个回调,这个回调立即执行,不会被添加到事件队列。
​

四、async 和 await 关键字

在Dart 1.9 中加入了async 和await关键字,有了这两个关键字,我们可以更简洁的编写异步代码了。

注意,async 不是并行执行,它是遵循Dart 事件循环规则来执行的,它仅仅是一个语法糖,简化Future API的使用。

async 关键字作为方法声明的后缀时,具有如下意义

  • 被修饰的方法会将一个 Future 对象作为返回值

  • 该方法会同步执行其中的方法的代码直到第一个 await 关键字,然后它暂停该方法其他部分的执行;

  • 一旦由 await 关键字引用的 Future 任务执行完成,await的下一行代码将立即执行。

    // 导入io库,调用sleep函数 import 'dart:io'; ​ // 模拟耗时操作,调用sleep函数睡眠2秒 doTask() async{ await sleep(const Duration(seconds:2)); return "Ok"; } ​ // 定义一个函数用于包装 test() async { var r = await doTask(); print(r); } ​ void main(){ print("main start"); test(); print("main end"); }

返回结果

main start
main end
Ok

五、Dart中Stream数据流

在Dart中,Stream 和 Future 一样,都是用来处理异步编程的工具。它们的区别在于,Stream 可以接收多个异步结果,而Future 只有一个。

Future 表示稍后获得的一个数据,所有异步的操作的返回值都用 Future 来表示。但是 Future 只能表示一次异步获得的数据。而 Stream 表示多次异步获得的数据。比如界面上的按钮可能会被用户点击多次,所以按钮上的点击事件(onClick)就是一个 Stream 。简单地说,Future将返回一个值,而Stream将返回多次值。

Stream 的创建可以使用 Stream.fromFuture,也可以使用 StreamController 来创建和控制。还有一个注意点是:普通的 Stream 只可以有一个订阅者,如果想要多订阅的话,要使用 asBroadcastStream()。

具体使用方式可以查看 Dart异步支持之Stream

六、Stream两种订阅模式及其调用

Stream有两种订阅模式:单订阅(single) 和 多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。

Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。

具体使用方式可以查看 Dart异步支持之Stream

七、await for 使用

await for是不断获取stream流中的数据,然后执行循环体中的操作。它一般用在知道stream什么时候完成,并且必须等待传递完成之后才能使用,不然就会一直阻塞。

Stream<String> stream = new Stream<String>.fromIterable(['不开心', '面试', '没', '过']);
main() async{
    await for(String s in stream){
    print(s);
  }
}

八、mixin机制

mixin 是Dart 2.1 加入的特性,以前版本通常使用abstract class代替。简单来说,mixin是为了解决继承方面的问题而引入的机制,Dart为了支持多重继承,引入了mixin关键字,它最大的特殊处在于:mixin定义的类不能有构造方法,这样可以避免继承多个类而产生的父类构造方法冲突。

mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件。

class A extend B with C,D{}
注意如果 CD都有要给run()方法,那么A.run()方法执行的是D里面的run方法,这个跟你写的顺序有关

九、矩阵最短路径和问题

给定一个矩阵m,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径种最小的路径和。如果给定的m如大家看到的样子,路径1,3,1,0,6,1,0是所有路径和最小的,所以返回12。

tips:

  • 矩阵大小为 M * N,dp[i][j]  的值表示从左上角,也就是(0,0) 位置,走到(i,j) 位置的最小路径和

  • dp[i][j] = map[i][j] + Math.min(dp[i-1][j] ,dp[i][j-1]) 

    public static int minmiumPath(int[][] map,int n,int m) {
        //新建个数组,n * m
        //dp[i][j]  的值表示从左上角,也就是(0,0) 位置,走到(i,j) 位置的最小路径和
        int[][] data = new int[n][m];
        data[0][0] = map[0][0];
        //第一列
        for(int i = 1;i<n;i++) {
            data[i][0] = data[i-1][0]+map[i][0];
        }
        //第一行
        for(int i = 1;i<m;i++) {
            data[0][i]= data[0][i-1] + map[0][i];
        }
    
        for(int i = 1;i< n;i++) {
            for(int j =1;j<m;j++) {
                //最短路径不是向下走就是向左走
                data[i][j]= map[i][j] + Math.min(data[i-1][j], data[i][j-1]);
            }
        }
        return data[n-1][m-1];
    
    }
    

笔记四