Flutter 异步编程之单线程下异步模型图文示例详解

目录
  • 一、 本专栏图示概念规范
    • 1. 任务概念规范
    • 2. 任务的状态
    • 3. 时刻与时间线
    • 4.同步与异步
  • 二、理解单线程中的异步任务
    • 1. 任务的分配
    • 2.异步任务特点
    • 3. 异步任务完成与回调
  • 三、 Dart 语言中的异步
    • 1.编程语言中与异步模型的对应关系
    • 2.Dart 编程中的异步任务
    • 3.当前任务分析
  • 四、异步模型的延伸
    • 1. 单线程异步模型的局限性
    • 2. 多线程与异步的关系
    • 3. Dart 中如何解决单线程异步模型的局限性

一、 本专栏图示概念规范

本专栏是对 异步编程 的系统探索,会通过各个方面去认知、思考 异步编程 的概念。期间会用到一些图片进行表达与示意,在一开始先对图中的元素和 基本概念 进行规范和说明。

1. 任务概念规范

任务 : 完成一项需求的基本单位。

分发任务: 触发任务开始的动作。

任务结束: 任务完成的标识。

任务生命期: 任务从开始到完成的时间跨度。

如下所示,方块 表示任务;当 箭头指向一个任务时,表示对该任务进行分发;任何被分发的任务都会结束。在任务分发和结束之间,有一条虚线进行连接,表示 任务生命期 。

2. 任务的状态

未完成 : Uncompleted
成功完成 : Completed with Success
异常结束 : Completed with Error

一个任务生命期间有三种状态,如下通过三种颜色表示。在 任务结束 之前,该任务都是 未完成 态,通过 浅蓝色 表示;任何被分发的任务都是为了完成某项需求,任何任务都会结束,在结束时刻根据是否完成需求,可分为 成功完成 和 异常结束 两种状态,如下分别用 绿色 和 红色 表示。

3. 时刻与时间线

机体 : 任务分发者或处理者。
时刻: 机体运行中的某一瞬间。
时间线: 所有时刻构成的连续有向轴线。

在一个机体运行的过程中,时间线是绝对的,通过 紫色有向线段 表示时间的流逝的方向。时刻 是时间线上任意一点 ,通过 黑点 表示。

4.同步与异步

同步 : 机体在时间线上,将任务按顺序依次分发。

同步执行任务时,前一个任务完成后,才会分发下一任务。意思是说: 任意时刻只有一个任务在生命期中。

异步: 机体在时间线上,在一个任务未完成时,分发另一任务。

也就是说通过异步编程,允许某时刻有两个及以上的任务在生命期中。如下所示,在 任务1 完成后,分发 任务2; 在 任务2 未结束的情况下,可以分发 任务 3 。此时对于任务 3 来说,任务 2 就是异步执行的。

二、理解单线程中的异步任务

上面对基本概念进行了规范,看起来可能比较抽象,下面我们通过一个小场景来理解一下。妈妈早上出门散步,临走前嘱咐:

小捷,别睡了。快起床,把被子晒一下,地扫一下。还有,没开水了,记得烧。

当前场景下只有小捷 一个机体,需要完成的任务有四个:起床、晒被、拖地 、烧水 。

1. 任务的分配

当机体有多个任务需要分发时,需要对任务进行分配。认识任务之间的关系,是任务分配的第一步。只有理清关系,才能合理分配任务。分配过程中需要注意:

[1] 任务之间可能存在明确的先后顺序,比如起床 需要在 晒被 之前。
[2] 任务之间先后顺序也可能无所谓,比如先扫地还是先晒被,并没有太大区别。
[3] 某类任务只需要机体来分发,生命期中不需要机体处理,并且和后续的任务没有什么关联性,比如烧水任务。

像烧水这种任务,即耗时,又不需要机体在任务生命期中做什么事。如果这类任务使用同步处理,那么任务期间机体能做的事只有 等待 。对于一个机体来说,这种等待就会意味着阻塞,不能处理任何事。

结合日常生活,我们知道当前场景之中,想要发挥机体最大的效力,最好的方式是起床之后,先分发 烧水任务,不需要等待烧水任务完成,就去执行晒被、扫地任务。这样的任务分配就是将 烧水 作为一个异步任务来执行的。

但在如果在分配时,将烧水作为最后一个任务,那么异步执行的价值就会消失。所以对任务的合理分配,对机体的处理效率是非常重要的。

2.异步任务特点

从上面可以看出,异步任务 有很明显的特征,并不是任何任务都有必要异步执行。特别是对于单一机体来说,任务生命期间需要机体亲自参与,是无法异步处理的。 比如一个人不能一边晒被 ,一边 扫地 。所以对于单线程来说,像一些只需要 分发任务,任务的具体执行逻辑由其他机体完成的任务,适合使用 异步 处理,来避免不必要的等待。

这种任务,在应用程序中最常见的是网络 io和 磁盘 io 的任务。比如,从一个网络接口中获取数据,对于机体来说,只需要分发任务来发送请求,就像烧水时只需要装水按下启动键一样。而服务器如何根据请求,查询数据库来返回响应信息,数据如何在网络中传输的,和分发任务的机体没有关系。磁盘的访问也是一样,分发读写文件任务后,真正干活的是操作系统。

像这类任务通过异步处理,可以避免在分发任务后,机体因等待任务的结束而阻塞。在等待其他机体处理的过程中,去分发其他任务,可以更好地分配时间。比如下面所示,网络数据获取 的任务分发后,需要通过网络把请求传输给服务器,服务器进行处理,给出响应结果。

整个任务处理的过程,并不需要机体参与,所以分发 网络数据获取 任务后,无需等待任务完成,接着分发 构建加载中界面 的任务,来展示加载中的界面。从而给出用户交互的反馈,而不是阻塞在那里等待网络任务完成,这就是一个非常典型的异步任务使用场景。

3. 异步任务完成与回调

前面的介绍中可以看出,异步任务在分发之后,并不会等待任务完成,在任务生命期中,可以继续分发其他任务。但任何任务都会结束,很多时候我们需要知道异步任务何时完成,以及任务的完成情况、任务返回的结果,以便该任务后续的处理。比如,在烧水完成之后,我们需要处理 冲水 的任务。

这就要涉及到一个对异步而言非常重要的概念:

回调: 任务在生命期间向机体提供通知的方式。

比如 烧水 任务完成后,烧水壶 “叮” 的一声通知任务完成;或者烧水期间发生故障,发出报警提示。这种在任务生命期间向机体发送通知的方式称为回调 。在编程中,回调一般是通过 函数参数 来实现的,所以习惯称 回调函数 。 另外,函数可以传递数据,所以通过回调函数不仅可以知道任务结束的契机,还可以通过回调参数将任务的内部数据暴露给机体。

比如在实际开发中,分发 网络数据获取 的任务,其目的是为了通过网络接口获取数据。就像烧开水任务完成之后,需要把 开水 倒入瓶中一样。我们也需要知道 网络数据获取 的任务完成的时机,将获取的数据 "倒入" 界面中进行显示。

从发送异步任务,到异步任务结束的回调触发,就是一个异步任务完整的 生命期。

三、 Dart 语言中的异步

上面只是介绍了 异步模型 中的概念,这些概念是共通的,无论什么编程语言都一样适用。就像现实中,无论使用哪国的语言表述,四则运算的概念都不会有任何区别。只是在表述过程中,表现形式会在语言的语法上有所差异。

1.编程语言中与异步模型的对应关系

每种语言的描述,都是对概念模型的具象化实现。这里既然是对 Flutter 中异步编程的介绍,自然要说一下 Dart 语言对异步模型的描述。

对于 任务 概念来说,在编程中和 函数 有着千丝万缕的联系:函数体 可以实现 任务处理的具体逻辑,也可以触发 任务分发的动作 。但我并不认为两者是等价的, 任务 有着明确的 目的性 ,而 函数 是实现这种 目的 的手段。在编程活动中,函数 作为 任务 在代码中的逻辑体现,任务 应先于 函数 存在。

如下代码所示,在 main 函数中,触发 calculate 任务,计算 0 ~ count 累加值和计算耗时,并返回。其中 calculate 函数就是对该任务的代码实现:

void main(){
  TaskResult result = calculate();
}
TaskResult calculate({int count = 10000000}){
  int startTime = DateTime.now().millisecondsSinceEpoch;
  int result = loopAdd(count);
  int cost = DateTime.now().millisecondsSinceEpoch-startTime;
  return TaskResult(
    cost:cost,
    data:result,
    taskName: "calculate"
  );
}
int loopAdd(int count) {
  int sum = 0;
  for (int i = 0; i <= count; i++) {
    sum+=i;
  }
  return sum;
}

这里 TaskResult 类用于记录任务完成的信息:

class TaskResult {
  final int cost;
  final String taskName;
  final dynamic data;
  TaskResult({
    required this.cost,
    required this.data,
    required this.taskName,
  });
  Map<String,dynamic> toJson()=>{
    "taskName":taskName,
    "cost":cost,
    "data": data
  };
}

2.Dart 编程中的异步任务

如下在计算之后,还有两个任务:saveToFile 任务,将运算结果保存到文件中;以及 render 任务将运算结果渲染到界面上。

void main() {
  TaskResult result = cacaulate();
  saveToFile(result);
  render(result);
}

这里 render 任务暂时通过在控制台打印显示作为渲染,逻辑如下:

void render(TaskResult result) {
  print("结果渲染: ${result.toJson()}");
}

下面是将结果写入文件的任务实现逻辑。其中 File 对象的 writeAsString 是一个异步方法,可以将内容写入到文件中。通过 then 方法设置回调,监听任务完成的时机。

void saveToFile(TaskResult result) {
  String filePath = path.join(Directory.current.path, "out.json");
  File file = File(filePath);
  String content = json.encode(result);
  file.writeAsString(content).then((File value){
    print("写入文件成功:!${value.path}");
  });
}

3.当前任务分析

如下是这三个任务的执行示意,在 saveToFile 中使用 writeAsString 方法将异步处理写入逻辑。

这样就像在烧水任务分发后,可以执行晒被一样。saveToFile 任务分发之后,不需要等待文件写入完成,可以继续执行 render 方法。日志输出如下:渲染任务的执行并不会因写入文件任务而阻塞,这就是异步处理的价值。

四、异步模型的延伸

1. 单线程异步模型的局限性

本文主要介绍 异步模型 的概念,认识异步的作用,以及 Dart 编程语言中异步方法的基本使用。至于代码中更具体的异步使用方式,将在后期文章中结合详细介绍。另外,一般情况下,Dart 是以 单线程 运行的,所以本文中强调的是 单线程 下的异步模型。

仔细思考一下,可以看出,单线程中实现异步是有局限性的。比如说需要解析一个很大的 json ,或者进行复杂的逻辑运算等 耗时任务,这种必须由 本机体 处理的逻辑,而不是 等待结果 的场景,是无法在单线程中异步处理的。

就像是 扫地 和 晒被 任务,对于单一机体来说,不可能同时参与到两个任务之中。在实际开发中这两个任务可类比为 解析超大 json 和 显示解析中界面 两个任务。如果前者耗时三秒,由于单线程 中同步方法的阻塞,界面就会卡住三秒,这就是单线程异步模型的 局限性。

2. 多线程与异步的关系

上面问题的本质矛盾是:一个机体无法 同时 参与到两件任务 具体执行过程中。解决方案也非常简单,一个人搞不定,就摇人呗。多个机体参与任务分配的场景,就是 多线程 。 很多人都会讨论 异步 和 多线程 的关系,其实很简单:两个机体,一个 扫地,一个 晒被,同一时刻,存在两个及以上的任务在生命期中,一定是异步的。毫无疑问,多线程 是 异步模型 的一种实现方式。

3. Dart 中如何解决单线程异步模型的局限性

像 C++ 、Java 这些语言有 多线程 的支持,通过 “摇人” 可以充分调度 CPU 核心,来处理一些计算密集型的任务,实现任务在时间上的最合理分配。

绝大多数人可能觉得 Dart 是一个单线程的编程语言,其实不然。可能是很多人并没有在 Flutter 端做过计算密集型的任务,没有对多线程迫切的需要。毕竟 移动/桌面客户端 大多是网络、数据库访问等 io 密集型 的任务,人手一个终端,没有什么高并发的场景。不像后端那样需要保证一个终端被百万人同时访问。

或者计算密集型的任务都有由平台机体进行处理,将结果通知给 Flutter 端。这导致 Dart 看起来更像是一个 任务分发者,发号施令的人,绝大多数时候并不需要亲自参与任务的执行过程中。而这正是单线程下的异步模型所擅长的:借他人之力,监听回调信息。

其实我们在日常开发中,使用的平台相关的插件,其中的方法基本上都是异步的,本质上就是这个原因。平台 是个烧水壶,烧水任务只需要分发 和 监听回调。至于水怎么烧开,是 平台 需要关心的,这和 网络 io 、磁盘 io 是很类似的,都是 请求 与 响应 的模式。这种任务,由单线程的异步模型进行处理,是最有效的,毕竟 “摇人” 还是要管饭的。

那如果非要在 Dart 中处理计算密集型的任务,该如何是好呢?不用担心,Dart 的 isolate 机制可以完成这项需求。关于这点,在后面会进行详述。认识 异步 是什么,是本文的核心,那本文就到这里,谢谢观看 ~

更多关于Flutter 单线程异步模型的资料请关注我们其它相关文章!

时间: 2022-09-17

Flutter学习LogUtil封装与实现实例详解

目录 一. 为什么要封装打印类 二. 需要哪些类 三. 打印输出的抽象类 四. 格式化日志内容 格式化堆栈 堆栈裁切工具类 格式化堆栈信息 格式化JSON 五. 需要用到的常量 六. 为了控制多个打印器的设置做了一个配置类 七. Log的管理类 九. 调用LogUtil 十. 定义一个Flutter 控制台打印输出的方法 十一. 现在使用前初始化log打印器一次 使用 一. 为什么要封装打印类 虽然 flutter/原生给我们提供了日志打印的功能,但是超出一定长度以后会被截断 Json打印挤在一

flutter使用tauri实现一个一键视频转4K软件

目录 前言 开发原因 工作原理 开发过程 前言 先说结论,tauri是一个非常优秀的前端桌面开发框架,但是,rust门槛太高了. 一开始我是用electron来开发的,但是打包后发现软件运行不是很流畅,有那么一点卡顿.于是为了所谓的性能,我尝试用tauri来做我的软件.在学了两星期的rust后,我发现rust真的太难学了,最后硬是边做边查勉强做出来了. 软件运行起来是比electron做的丝滑很多,但是写rust真的是很痛苦,rust的写法和其他语言是截然不同的,在不知道之前我是个rust吹,觉

Flutter实现一个支持渐变背景的Button示例详解

目录 Flutter中的按钮 不完美的地方 在child中处理 外面套一个wrapper MaterialStateProperty MaterialStatesController 边距问题 EnhancedButton Flutter中的按钮 自Flutter 1.20 新增了ButtonStyleButton 系列按钮,可以说非常好用了,默认样式比之前漂亮了许多,扩展性也增加了很多.按钮样式统一由ButtonStyle这个类提供,支持根据各种状态(MaterialState)变化的属性,也

Flutter 假异步的实现示例

就像 android 有 handle 一样,消息队列这东西好像还真是系统必备,Flutter 也有自己的消息队列,只不过队列直接封装在了 Dart 的线程类型 Isolate 里面了,不过 Flutter 还是提供了 Futrue 这个 API 来专门来操作各种消息,以及实现基于消息队列的假异步 Flutter 的"异步"机制 这里的异步是加了引号的,可见此异步非真异步,而是假异步.Flutter 的 异步 不是开新线程,而是往所属线程的 消息队列 中添加任务,当然大家也可以按上文那

详解flutter中常用的container&nbsp;layout实例

目录 简介 Container的使用 旋转Container Container中的BoxConstraints 总结 简介 在上一篇文章中,我们列举了flutter中的所有layout类,并且详细介绍了两个非常常用的layout:Row和Column. 掌握了上面两个基本的layout还是不够的,如果需要应付日常的layout使用,我们还需要掌握多一些layout组件.今天我们会介绍一个功能强大的layout:Container layout. Container的使用 Container是一

Android Flutter实现精灵图的使用详解

目录 前言 如何使用精灵图 自定义实现加载 Flame加载精灵图 前言 在日常开发中遇到的图片展示一般是静态图和Gif图两种形式(静态和动态的不同).与此同时当需要对图片做效果时让其动起来,常用方案是Gif图播放或者是帧动画(多种静态图轮询播放).但在游戏开发中还有一种动图表现形式叫做Sprite图(雪碧图),其在前端开发中也是很常见.为什么需要使用精灵图,因为每张图片显示都需要去发起请求获取,若页面图片数量较多(一个页面有几十个小图)并发请求将是一个大数量级,可能会造成页面加载速度降低,精灵图

Android 自定义imageview实现图片缩放实例详解

Android 自定义imageview实现图片缩放实例详解 觉得这个自定义的imageview很好用 性能不错  所以拿出来分享给大家  因为不会做gif图  所以项目效果 就不好贴出来了  把代码贴出来 1.项目结构图 2.Compat.class package com.suo.image; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.view.View; pu

Android 带logo的二维码详解及实例

Android 带logo的二维码详解及实例 好久没有写博客了,快元旦了公司的事情也不是很多,刚好和朋友一起出去玩玩,朋友是搞PHP的说到了每天在公司都是搞些什么二维码和微信支付的相关东西,因为上班的时间不忙,所以随便来搞下. 二维码(Quick Response Code),又称二维条码,它是用特定的几何图形按一定规律在平面(二维方向)上分布的黑白相间的图形,是所有信息数据的一把钥匙.在现代商业活动中,如果一个产品是不能通过二维码来进行访问什么的,显然是不成功的.用的比较多的生成二维码的jar

Android Studio 修改应用包名实例详解

Android Studio 修改应用包名实例详解 我们平时新建项目有些朋友可能当时就是随意写的一个包名,然后在项目过程中, 又感觉这个包名不太好,所以就要对包名进行修改,根据我们正常的修改方式,是这样的. 在种情况是只能修改最外层的那个名称, 如果我们现在是需要修改中间的某一个,这里就行不通了. 那么我们来看一下如何修改成你最终要的包名. 操作图如下: 看到没有,我们只需要在setting里面,把 compact empty middle packages 这个选项去掉,这样,我们的包的层次结

Android OkHttp的简单使用和封装详解

Android OkHttp的简单使用和封装详解 1,昨天把okHttp仔细的看了一下,以前都是调用同事封装好了的网络框架,直接使用很容易,但自己封装却不是那么简单,还好,今天就来自我救赎一把,就和大家写写从最基础的OKHttp的简单get.post的使用,再到它的封装. 2,OkHttp的简单使用 首先我们创建一个工程,并在布局文件中添加三个控件,TextView(用于展示获取到json后的信息).Button(点击开始请求网络).ProgressBar(网络加载提示框) ①简单的异步Get请

Android几种多渠道打包的步骤详解

1.什么是多渠道打包 在不同的应用市场可能有不同的统计需求,需要为每个应用市场发布一个安装包,这里就引出了Android的多渠道打包.在安装包中添加不同的标识,以此区分各个渠道,方便统计app在市场的各种. 2.几种打包方式 友盟 UMeng Android Studio自带 美团 Walle 3.开始使用 3.1 友盟UMeng 第一步:在AndroidManifest中添加 <meta-data android:name="UMENG_CHANNEL" android:val

Android开心消消乐代码实例详解

突然想要在android上写一个消消乐的代码,在此之前没有系统地学过java的面向对象,也没有任何android相关知识,不过还是会一点C++.8月初开始搭建环境,在这上面花了相当多的时间,然后看了一些视频和电子书,对android有了一个大概的了解,感觉差不多了的时候就开始写了. 疯狂地查阅各种资料,反反复复了好几天后,也算是写出了个成品.原计划有很多地方还是可以继续写下去的,比如UI设计,比如动画特效,时间设计,关卡设计,以及与数据库的连接,如果可以的话还能写个联网功能,当然因为写到后期内心

android studio打印日志语句Log.d()详解

Log.d()方法内需要传入两个参数. 1.第一个参数时tag,一般传入类名,用于对打印信息进行过滤: 2.第二个参数,是一个字符串类型的msg,表示你想要打印的内容. 输出Log.d()语句的快捷键为: logd+tab键 在我们每写一条Log.d()语句时,就要传入一次tag参数,而每一次的tag参数值基本是一样的,这样就会很麻烦,其实只要我们在类中创建一个字符串类型的变量TAG,那么在我们每次写log.d()语句的时候,系统就会自动将该TAG的值传入tag参数中 自动生成一个以当前类名作为

Android 通过网络图片路径查看图片实例详解

Android 通过网络图片路径查看图片实例详解 1.在项目清单中添加网络访问权限 <!--访问网络的权限--> <uses-permission android:name="android.permission.INTERNET"/> 2.获取网络图片数据 /** * 获取网络图片的数据 * @param path 网络图片路径 * @return * @throws Exception */ public static byte[] getImage(Str

Android Naive与WebView的互相调用详解

Android  Naive与WebView的互相调用详解 Android的Naive程序是可以嵌套WebView,并且可以做到与WebView的交互,一般来说有两种方法,一是直接交互,比如,Naive直接调用WebView的方法和WebView直接调用Naive的方法.二是WebView可以写<a/>超链接标签,然后用户点击此标签时,Naive可以拦截到点击标签的事件,这样,我们可以在链接上做一套自己的协议,然后Android和iOS可以根据此协议做出相同的处理,做到多平台统一. 我们先研究

Android 拦截返回键事件的实例详解

Android 拦截返回键事件的实例详解 KeyEvent类 Android.View.KeyEvent类中定义了一系列的常量和方法,用来描述Android中的 按键事件和返回键有关的常量和方法有. KeyEvent.KEYCODE_BACK: 表示key类型为返回键 KeyEvent.ACTION_DOWN:表示事件为按下key,如果一直按住不放,则会不停产生此事件. KeyEvent.ACTION_UP:表示事件为为放开key,一次点击key过程只会调用一次. public final in