这篇文章并不打算去剖析协程的概念和原理等东西,相似的文章在网上已经有不少了,相信不少文章解释得比我更准确和透彻,若是你们感兴趣的话能够自行查阅学习。html
对于协程还仅限于据说的程度的同窗,能够查阅一下这些资料:git
协程指南github
官方文档,中文,能够直连,强烈推荐。如果有条件能够尝试看英文版,由于协程还有一部分API属于实验阶段,有可能会产生变化,英文文档能够看第一手资料。bash
破解Kotlin协程服务器
写得很好的一个系列文章,不管是从入门或是加深理解都推荐看一看。框架
Kotlin 协程的挂起好神奇好难懂async
扔老师的两个视频,说得很浅显易懂,看完基本上至少能了解协程究竟是个什么玩意ide
网上有不少协程+Retrofit或
是协程+XXX框架
进行一系列封装的文章,我也不过多赘述,但若是有同窗说,我就想单独用协程呢?仅协程这个单独的个体来讲,能给实际开发解决什么痛点?函数
这一系列文章会假设你们对于协程有基本上的了解但不知道怎么去用(实际上不了解也不要紧,看完文章你至少可以知道协程带来的好处),从最基础的粒度上告诉你们,哪些场景下可使用协程?能够带来哪些好处?
万物始于CoroutinScope
,可能有一些老的文章还在使用GlobalScope.launch
来教你怎么启动一个协程,那么这么用有问题吗?固然没问题了,连官方文档的第一篇入门文章也是这么教的,只是这么用的话,会须要你注意自行处理协程的开始和结束等问题以避免致使内存泄漏或是其它你不想看见的异常,若是有更好的选择的话,何苦为难本身?
实际上如今Android官方的Jetpack
组件在不少状况下已经提供给开发者默认的Scope,好比ViewModelScope
、LifecycleScope
等,不过这不是这篇文章的重点,前言已经说了,从最基础的粒度上对吧。那么抛弃这些框架组件来讲,我推荐你用什么呢?
val mainScope = MainScope()
复制代码
就这么简单,一行代码。让咱们来看看Kotlin协程官方提供的这个MainScope()
是什么东西:
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
复制代码
很简洁明了,这是一个内部的取消操做只会向下传播,运行在主线程
的CoroutineScope
;
若是你想要一个默认运行在子线程
的Scope:
val ioScope = MainScope() + Dispatchers.IO
复制代码
或者你想要一个自动捕获内部包括子协程可能抛出的异常的Scope:
val mainScope = MainScope() + CoroutineExceptionHandler { context, throwable ->
throwable.printStackTrace()
}
复制代码
我推荐你这么使用的目的是,你能够在Activity的OnDestroy
生命周期或者其它任何你须要的时机里调用mainScope.cancel()
,便可在大部分状况下
取消全部可能还在运行的子协程;
为何是大部分状况下?由于在某些时候,好比子协程
里进行循环读取文件流等阻塞操做时,你可能须要自行加上判断Scope的运行状态进行中断操做。
Scope说完了,如今说说最简单的一个函数:launch
假设你如今有某一部分代码,你只是简单的想延迟几秒后执行,你不想用postDelay不想写Timer等等,更不想在延迟的那几秒内阻塞主线程(这不是废话么;D),你能够这么写:
private fun delayToExecute(duration: Long, execution: () -> Unit) = mainScope.launch {
delay(duration)
execution()
}
复制代码
这里咱们将launch
放到了方法上,实际上这个方法返回一个Job
,若是你不须要的话,彻底能够不去管它;而且咱们直接使用的是mainScope.launch
,没有指定其它Dispatchers
,所以execution
函数体会在主线程里执行,能够直接在里面作修改UI
等操做,你如今能够这么调用:
delayToExecute(3000) {
textView.text = "xxxx"
}
复制代码
你也能够将launch
放到调用者上,将前面的方法改成:
private suspend fun delayToExecute(duration: Long, execution: () -> Unit) {
delay(duration)
execution()
}
复制代码
调用的时候就得改成:
mainScope.launch {
delayToExecute(3000) {
textView.text = "xxxx"
}
}
复制代码
两种方法有什么区别呢?去查阅一下suspend标识符的意义吧,前言里扔老师的视频对于suspend的讲解很清楚,这里就不过多赘述
若是你但愿execution
函数体执行在子线程上的话也很简单,launch
支持指定Dispatchers
,加一个参数便可:
mainScope.launch(Dispatchers.IO)
复制代码
是否是以为太简单了?就这?别急,launch
的本质目的只是让你启动一个基础协程,有了它,你才可以跟各类以后要说的各类suspend
方法以及其余协程函数打交道。
上面说了launch
本质上只是启动一个基础协程,它返回一个Job
。
假设如今你有一个或多个函数,你但愿它运行在子线程上,里面进行一些耗时处理,一段时间后返回处理结果,当且仅当全部这些函数全都返回告终果以后,才进行后续的处理。
想想这种状况下,若是不使用RxJava
,是否是须要写一堆的回调去接收和处理结果?甚至即便使用RxJava
,是否是也或多或少的存在一些嵌套代码?
这时候就是协程最让开发者舒服的时候了,即所谓的用同步风格,写异步代码。
咱们将这种状况拆开了看,这些运行在子线程上的函数能够想象为处理者
,收到一些数据,将数据处理以后返回结果,而调用者
但愿以最简单的形式去使用这些处理者
,也就是调起处理者
,以返回值的形式接收结果,不要再让调用者
去写回调接收结果。
咱们来看看假设有两个处理者
,怎么去实现:
private fun processA(data: String): String {
info { "${Thread.currentThread().name}: process A start" }
Thread.sleep(4000)
info { "${Thread.currentThread().name}: process A done" }
return "$data --> process A done"
}
private fun processB(data: String): String {
info { "${Thread.currentThread().name}: process B start" }
Thread.sleep(2000)
info { "${Thread.currentThread().name}: process B done" }
return "$data --> process B done"
}
复制代码
processA
函数在4秒后返回处理结果,processB
在2秒后返回处理结果。
再来看看调用者
怎么实现:
private fun invokeProcess() = mainScope.launch {
val defA = async(Dispatchers.IO) {
processA("Data for A")
}
val defB = async(Dispatchers.IO) {
processB("Data for B")
}
val resultA = defA.await()
val resultB = defB.await()
info { "${Thread.currentThread().name}: $resultA \t $resultB" }
}
复制代码
以后执行invokeProcess()
,打印出来的日志为:
DefaultDispatcher-worker-2: process A start
DefaultDispatcher-worker-1: process B start
DefaultDispatcher-worker-1: process B done
DefaultDispatcher-worker-2: process A done
main: Data for A --> process A done Data for B --> process B done
复制代码
从日志上能够清晰的看出来,async
函数在调用时即马上执行(也能够根据须要指定不当即执行,具体能够了解一下async
接收的CoroutineStart
参数)
从通俗的角度上解释一下async
这个函数,它会启动一个子协程,将lambda表达式
(即上例中的处理者
函数)运行在指定的线程中,而且返回一个Deferred
对象,该对象的await()
方法的返回值即async
中lambda表达式
内的返回值。
在调用对应的Deferred
的await()
方法时,若函数还在处理中,则挂起当前协程(即上例中的调用者所在的协程)等待返回值;若函数此时已经有返回值,则当即获得结果。
这里仅仅只介绍了async
函数最基础的用法,感兴趣的同窗能够看看官方文档组合挂起函数了解更多可配置的参数。
可能有的同窗又说了,不就是async
、Deferred
嘛,相似的东西Java甚至其它语言也有啊。
那么下面就来讲说重头戏,Kotlin协程是怎么真正解决回调地狱的场景的。
suspendCoroutine
在上面的例子中,细心的同窗可能发现了,处理者
中的结果都是以return
的方式返回的,然而实际开发中,颇有可能处理者
脱离了你的控制,没办法以return
的方式返回结果。
好比说你可能须要在处理者
中调用某个第三方框架,这个第三方框架限制了你必须以回调的形式来接收结果;在这种状况下,处理者
没法避免的涉及到一些回调嵌套,那么咱们看看怎么样让调用者
最大限度的避免回调地狱。
以我我的很喜欢的一个动态权限处理框架AndPermission来做为例子,这个框架帮助开发者去处理动态权限的判断和申请,以回调的形式接收结果,大概是这个样子的:
AndPermission.with(this)
.runtime()
.permission(Manifest.permission.CAMERA)
.onGranted {
// 已有权限或是用户点击了授予权限
}
.onDenied {
// 没法获取权限或是用户点击了拒绝授予权限
}
.start()
复制代码
能够看到,这个例子里,权限的是否获取是经过onGranted
和onDenied
来回调的。
若是不使用协程来的话,是否是得在那两个回调里嵌套进拿到权限结果后的逻辑代码?这仍是只嵌套了一层的状况,假设有更多的相似这样的嵌套状况呢?
让咱们换回刚才说aync
时候的思路,把这个问题当作调用者
和处理者
的关系:
权限的申请应该是一个独立的处理者
,内部的逻辑不该该须要调用者
去关注;
对于调用者
来讲,权限的申请只应该关心最终结果,也就是true
或者false
就够了。
如今来看看权限申请的处理者
怎么实现:
private suspend fun checkPermission(context: Context, vararg permissions: String) =
suspendCoroutine<Boolean> { continuation ->
AndPermission.with(context)
.runtime()
.permission(permissions)
.onGranted {
// 已有权限或是用户点击了授予权限
continuation.resume(true)
}
.onDenied {
// 没法获取权限或是用户点击了拒绝授予权限
continuation.resume(false)
}
.start()
}
复制代码
这里稍微将参数改造了一下,甚至能够将这个方法抽为一个工具类的静态方法,在任何须要判断权限的时候均可以使用。
suspendCoroutine
函数接收一个lambda表达式
,调起的时候挂起调用者,并continuation.resume()
将结果以返回值的形式返回给调用者。
有点拗口?没事,再来看看调用者
的代码:
private fun startCamera() = mainScope.launch {
val permission = checkPermission(context, Manifest.permission.CAMERA)
if (permission) {
// 得到了权限,开启摄像头或其余操做
} else {
// 未得到权限,提醒用户使用该权限的目的
}
}
复制代码
调用者
的代码是否是简洁明了许多?调用处理者
,接收一个布尔值,而后就直接能够进行后续的逻辑处理了。
以此延伸,无论你有多少个相似这样的异步返回结果的处理者
的时候,对于调用者来讲都不要紧,通通都是一行代码,接收一个返回值,搞定,是否是很方便?
留一个很常见的场景你们能够尝试本身去实现一下试试:
读取SD卡内的某张图片,对其进行压缩,再将其上传到服务器上
这个系列的第一篇文章就到此结束了,仅仅从最基本的角度讲了Kotlin协程几个函数在默认状况下的使用场景,有兴趣的同窗能够自行看一下官方文档,了解一下这几个函数的一些可选参数:)