✨学前温习
线程
线程是执行代码的一种路径,每一行代码对应的一或多条相关指令,指令将会在同一线程上按顺序执行。而在线程的执行中 调度程序会为每一个线程分配一个时间片,线程依据时间片算法进行调度。相关线程要么在该时间片内完成,要么挂起,直到获得另一个时间片。 而在我们的实际项目运行过程中,除了主线程外还有其它线程。处理器将在不同系列的指令之间来回切换,呈现多任务处理的状态。使多个代码乱序执行,或近乎于并发执行,从而更有效地利用资源,操作系统可以根据系统,编程语言和并发单元的特性来管理多任务。如应用界面响应用户操作,应用在后台执行复杂的任务(如网络请求、下载图片),是一种"并发"的概念。
而利用线程简单实现多任务和并发的方式,可能会出现很多问题
由于主线程负责运用应用界面,因此,该线程具有较高的性能,以便应用能够顺畅运行,任何长时间运行的任务在完成之前都会阻塞该线程,从而导致应用无响应。
目前,手机每秒会尝试更新界面60到120次,在60的刷新率下,Android每次刷新界面所需的时间为16ms或者更低,当主线程来不及更新时,Android会中止尝试完成单个更新周期,既丢帧,以试图跟上进度。丢帧和帧数波动属于正常现象。但是丢帧过多会导致应用无响应。
因此,我们需要处理这种并发过程中的调度问题,使应用程序在这种操作过程中不至于阻塞,而造成应用崩溃。
而在多线程中,我们还会遇到竞争,多个线程同时访问内存中的同一个值,从而导致出现脏数据,从而导致应用崩溃。
为此,官方推出了协程的kotlin功能来编写清晰的非阻塞并发代码。
协程作用
- 处理耗时任务 这种任务常常会阻塞主线程
- 保证主线程安全 确保安全地从主线程调用任何suspend 函数
AsyncTask 和 协程
但是,我们之前在开发过程中已经学习过了异步,可以利用异步处理耗时操作,再切回主线程执行UI响应。这和我们的协程有什么区别么?
AsyncTask
AsyncTask是异步操作。下面让我们来看一段异步的代码
val submitButton = findViewById<Button>(R.id.bt_submit_main).also { it.setOnClickListener { object : AsyncTask<Void, Void, Int>(){ override fun doInBackground(vararg params: Void?): Int { //耗时操作 return 5; } //回调方法 耗时操作完成后的返回值 override fun onPostExecute(result: Int?) { if (result != null) { nameTv.setText(result) } } }.execute() }
我们可以看到,在利用Async实现异步的过程中,我们在doInBackground
内进行了耗时操作,然后我们覆写其onPostExecute
回调方法,在回调方法中,获取刚才耗时操作的结果,进行相关处理。 我们再来对比一下协程对于并发过程中的处理操作
GlobalScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO){ //耗时操作 delay(1200) } //处理结果 }
我们可以看到,通过协程的处理,我们的程序呈现一种同步代码的感觉,这是协程的一种异步代码同步化,这是一个方面。
另外一个方面,在我们通过async的处理过程中,我们可以看到它的处理过程是通过回调进行结果处理的,而回调,是有可能造成回调地狱的,因此,我们可以尝试使用协程处理这种并发操作。
并且协程可以实现轻量级的并发
实现更高的资源利用率
协程挂起与恢复
常规函数基本操作包含了invoke和return。协程的加入增加了suspend 和 resume
- suspend 挂起或暂停 用于暂停执行当前协程 并保存所有局部变量
- resume 用于让已暂停的协程从其暂停处继续执行
第一个协程程序
fun main() { GlobalScope.launch(Dispatchers.Main) { // 在后台启动一个新的协程并继续 delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒) println("World!") // 在延迟后打印输出 } println("Hello,") // 协程已在等待时主线程还在继续 Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活 }
代码挂起与阻塞 挂起的作用
上面那段代码,我们可以看到挂起与阻塞
- 挂起 非阻塞的 当我们在点击事件中执行挂起函数,我们会发现,onclick()事件很快跳出循环,并在阻塞操作完成后继续完成后续操作
- 阻塞 而阻塞操作 我们在点击事件中执行会造成阻塞的函数,我们会发现,onclick()函数并没有完成,一直等到耗时操作完成后完成后续操作才会onclick()函数。只是一个阻塞过程。当多次在点击事件中执行阻塞函数,当达到一定的耗时时,会造成应用程序无响应,从而造成应用崩溃。
上面我们看到了协程的一部分实现以及挂起的一些操作
而我们的协程实现由两部分构成
- 基础设施层 标准库的协程API,主要对协程提供了概念和语义上最基本的支持
- 业务框架层 协程的上传框架支持
我们刚才用的都是业务框架层实现,而在基础设施层对协程的使用,是比较复杂的,下面让我们看看具体的代码实现
//这只是创建了一个协程 val continuation =suspend { //协程体 5 }.createCoroutine(object : Continuation<Int>{ //CoroutineContext 协程上下文 override val context: CoroutineContext get() = EmptyCoroutineContext; //把协程体的执行结果返回 这是一个回调 实际上生成的代码有回调 override fun resumeWith(result: Result<Int>) { println(result) } }) //该实例对象保存了挂起点信息 需要利用resume启动协程 continuation.resume(Unit)
基础设施层中我们调用的是kotlin包
业务框架层中我们调用的是kotlinx包
我们可以比较直观的感受到业务框架层中的异步代码同步化的直观、简单、以及没有回调。 而在基础设施层中,当我们需要使用协程,在使用suspend
写入协程体,利用createCoroutine
进行创建协程时,是比较复杂的,并且在构建Continuation
内部类的过程中,我们发现还是会涉及到回调的使用。而在这之中,我们看到Continuation
保存挂起点,而在Continuation
中,实际上是CoroutineContext
协程上下文保存挂起点信息 我们可以进入Continuation
的实现代码
@SinceKotlin("1.3") public interface Continuation<in T> { /** * The context of the coroutine that corresponds to this continuation. */ public val context: CoroutineContext /** * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the * return value of the last suspension point. */ public fun resumeWith(result: Result<T>) }
我们可以看到这是一个接口,而这之中,只有以一个CoroutineContext
实例变量。因此、由协程上下文保存挂起点信息
协程调度器
而所有的协程都需要在协程调度器上才可以实现 在kotlin中,主要应用到了这几种协程调度器 调度器种类
Dispatchers.Main
- Android上的主线程 用来处理UI交互和一些轻量级任务
- 调用suspend函数
- 调用UI函数
- 更新LiveData
Dispatchers.Default
- 非主线程 专为CPU密集型任务进行了优化
- 数组排序
- JSON数据解析
- 处理差异判断
Dispatchers.IO
- 非主线程 专为磁盘和系统IO进行了优化
- 数据库
- 网络
- 文件读写
协程 任务泄漏
在我们的实现过程中,会出现以下场景 打开活动 to 开启协程进行网络请求 to 回退到上一活动 这个时候活动如果已经销毁,但是网络请求还在 会造成任务泄漏
这个时候,协程任务会丢失,导致无法追踪,会导致内存 CPU 磁盘等资源浪费 甚至发送 一个无用的网络请求 这种情况称之为任务泄漏
为了能够避免 任务泄漏 kotlin引入结构化并发机制
任务泄漏解决 结构化并发
结构化并发可以实现以下功能
- 取消任务
- 当某项任务不再需要时取消
- 追踪任务
- 当任务正在执行时 对任务进行追踪
- 发出错误信号
- 当协程失败时 发出错误信号表明有错误发生
定义协程必须指定CoroutineScope
它会跟踪所有协程 ,同样可以取消由它启动的所有协程
指定CoroutineScope便可以进行结构并发
- GlobalScope 生命周期是process级别 即使Activity 或 Fragment已经被销毁 协程仍然在执行
- MainScope 在 Activity中使用 可以在onDestory中取消协程
- viewModelScope 只能在ViewModel中使用,绑定ViewModel的生命周期
- lifecycleScope 只能在Activity、Fragment中使用 会绑定Activity和Fragment的生命周期
实战中如何验证结构化并发可以解决任务泄漏。
我们可以通过捕获异常来观察协程是否被取消 因为协程取消会抛出异常
Android小白,请多多指教。
内容如有错误,希望得到指点