本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布android
协程经过替代回调(callback)来简化异步代码git
听起来蛮抽象的,来看代码github
fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
复制代码
Android系统为了保证界面的流畅和及时响应用户的输入事件,主线程须要保持每16ms一次的刷新(调用 onDraw()
函数),因此不能在主线程中作耗时的操做(好比 读写数据库,读写文件,作网络请求,解析较大的 Json 文件,处理较大的 list 数据)。数据库
get()
经过接口获取用户数据,若是在主线程中调用fetchDocs()
函数就会阻塞(block)主线程,App 会卡顿甚至崩溃。数组
因此须要在子线程中调用get()
函数,这样主线程就能够刷新界面和处理用户输入,待get()
函数执行完毕后经过 callback 拿到结果。安全
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
复制代码
suspend fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
suspend fun get(url: String) =
withContext(Dispatchers.IO) {
...
}
复制代码
明明是同步的写法为何不会阻塞主线程? 对,由于suspend
微信
被suspend
修饰的函数比普通函数多两个操做(suspend 和 resume)网络
get()
函数一样也是一个suspend
函数。架构
suspend
修饰的函数并不意味着运行在子线程中并发
若是须要指定协程运行的线程,就须要指定Dispatchers ,经常使用的有三种:
经过withContext()
能够指定Dispatchers,这里的get()
函数里的withContext
代码块中指定了协程运行在Dispatchers.IO中。
来看下这段代码的具体执行流程
suspend
修饰的函数的时候,Kotlin须要追踪正在运行的协程而不是正在执行的函数suspend
的标记,绿色上面的是协程,绿色下面的是一个正常的函数fetchDocs()
函数,在调用栈上加一个 entry,这里也存储着fetchDocs()
函数的局部变量suspend
函数的调用(这里指的是 get()
函数调用),这时候Kotlin要去实现suspend
操做(将函数的状态从堆栈复制到一个地方,以便之后保存,全部suspend
的协程都会被放在这里)get()
函数,一样新建一个entry,当调用到withContext()
(withContext函数被 suspend 修饰)的时候,一样 执行suspend操做(过程和前面同样)。此时主线程里的全部协程都被 suspend,因此主线程能够作其余事情(执行 onDraw,响应用户输入)fetchDocs()
发成错误,会在这里抛出异常val viewModelJob = Job() //用来取消协程
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) //初始化CoroutineScope 指定协程的运行所在线程传入 Job 方便后面取消协程
uiScope.launch { //启动一个协程
updateUI() //suspend函数运行在协程内或者suspend另一个函数内
}
复制代码
suspend fun updateUI() {
delay(1000L) //delay是一个 suspend 函数
textView.text = "Hello, from coroutines!"
}
复制代码
viewModelJob.cancel()//取消协程
复制代码
CoroutineScope
,为何须要? 一会解释CoroutineScope
接受CoroutineContext
做为参数,CoroutineContext
由一组协程的配置参数组成,能够指定协程的名称,协程运行所在线程,异常处理等等。能够经过plus
操做符来组合这些参数。上面的代码指定了协程运行在主线程中,而且提供了一个Job
,可用于取消协程
CoroutineName
(指定协程名称)Job
(协程的生命周期,用于取消协程)CoroutineDispatcher
,能够指定协程运行的线程Coroutine builders
来启动协程,协程运行在Coroutine builders
的代码块里面
Job
,可用来取消协程;有异常直接抛出CoroutineContext
改变协程运行的上下文Structured concurrency
)若是在
foo
里协程启动了bar
协程,那么bar
协程必须在foo
协程以前完成
foo
里协程启动了bar
协程 ,可是bar
并无在 foo
完成以前执行完成,因此不是结构化并发
foo
里协程启动了
bar
协程 ,而且
bar
在
foo
完成以前执行完成,因此是结构化并发
结构化并发可以带来什么优点呢?下面一点点阐述。
尽管协程自己是轻量级的,可是协程作的工做通常比较重,好比读写文件或者网络请求。使用代码手动跟踪大量的协程是至关困难的,这样的代码比较容易出错,一旦对协程失去追踪,那么就会致使泄漏。这比内存泄漏更加严重,由于失去追踪的协程在resume
的时候可能会消耗内存,CPU,磁盘,甚至会进行再也不必要的网络请求。
如何避免泄漏呢?这其实就是CoroutineScope
的做用,经过launch
或者async
启动一个协程须要指定CoroutineScope
,当要取消协程的时候只须要调用CoroutineScope.cancel()
,kotlin 会帮咱们自动取消在这个做用域里面启动的协程。
结构化并发能够保证当一个做用域被取消,做用域里面的全部协程会被取消
若是使用架构组件(Architecture Components),比较适合在ViewModel
中启动协程,而且在onCleared
回调方法中取消协程
override fun onCleared() {
super.onCleared()
viewModelJob.cancel() //取消ViewModel中启动的协程
}
复制代码
本身写CoroutineScope比较麻烦,架构组件提供了viewModelScope
这个扩展属性,能够替代前面的uiScope
。
看下viewModelScope
这个扩展属性是如何实现的:
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(Job() + Dispatchers.Main))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
复制代码
一样是初始化一个CoroutineScope,指定Dispatchers.Main和 Job
##ViewModel
@MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
onCleared();
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
复制代码
clear()
中会自动取消做用域中的协程。有了viewModelScope
这个扩展属性能够少些不少模板代码。
再看一个稍复杂的场景,同时发起两个或者多个网络请求。这就意味着要开启更多的协程,随处开启协程可能致使潜在的泄漏问题,调用者可能不知道新开启的协程,所以也无法追踪他们。 这时候就须要coroutineScope
或者supervisorScope
(注意不是CoroutineScope
)。
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
launch { fetchDoc(2) }
}
}
复制代码
这个示例中,同时发起两个网络请求。在suspend 函数里面能够经过coroutineScope
或 supervisorScope
安全地启动协程。为了不泄漏,咱们但愿fetchTwoDocs
这样的函数返回的时候,在函数内部启动的协程都能执行完成。
结构化并发保证当suspend函数返回的时候,函数里面的全部工做都已经完成
Kotlin能够保证使用coroutineScope
不会从fetchTwoDocs
函数中发生泄漏,coroutineScope
会suspend
本身直到在它里面启动的全部协程执行完成。正是由于这样,fetchTwoDocs
不会在coroutineScope
内部启动的协程完成前返回。
若是有更多的协程呢?
suspend fun loadLots() {
coroutineScope {
repeat(1000) {
launch { fetchDoc(it) }
}
}
}
复制代码
这里在suspend函数中启动了更多的协程,会泄露吗?并不会。
因为这里的loadLots
是一个suspend
函数,因此loadLots
函数会在一个CoroutineScope
中被调用,coroutineScope
构造器会使用这个CoroutineScope
做为父做用域生成一个新的CoroutineScope
。在coroutineScope
代码块内部,launch
函数会在这个新的CoroutineScope
中启动新的协程,这个新的CoroutineScope
会追踪这些新的协程,当全部的协程执行完毕,loadLots
函数才会返回。
coroutineScope
和supervisorScope
会等到全部的子协程执行完毕。
使用coroutineScope
或者 supervisorScope
能够安全地在suspend
函数里面启动新的协程,不会形成泄漏,由于老是会suspend
调用者直到全部的协程执行完毕。coroutineScope
会新建一个子做用域(child scope),因此若是父做用域被取消,它会把取消的信息往下传递给全部新的协程。
另外coroutineScope
和supervisorScope
的区别在于:coroutineScope
会在任意一个协程发生异常后取消全部的子协程的运行,而supervisorScope
并不会取消其余的子协程。
前面有介绍过async
里面若是发生异常是不会直接抛出的,直到 await
获得调用,因此下面的代码不会抛出异常。
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
复制代码
可是coroutineScope
会等到协程执行完毕,因此发生异常后会抛出。下面的代码会抛出异常。
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
复制代码
结构化并发保证当协程出错时,协程的调用者或者他的作用户会获得通知
因而可知 结构化并发能够保证代码更加安全,避免了协程的泄漏问题
suspend
函数返回,里面的工做能保证完成,于是能够追踪正在执行的任务参考文档:
Coroutines on Android (part I): Getting the background
Coroutines on Android (part II): Getting started