一些问题
程序什么时候需要切线程?
- 工作比较耗时:放在后台
- 工作特殊:需要放在指定线程(ui刷新、计算、io)
kotlin的协程是什么?
线程框架
可以用同步代码写出异步操作
suspend 关键字是什么?
- 并不是切线程关键字
- 用于标记和提醒
协程的优势是什么?
耗时代码自动后台,提高软件性能
线程执行完毕自动切回起始线程
协程的使用
基本使用
- 用
launch()
开启一段协程,一般需要指定Dispatchers.Main
- 把要在后台工作的函数,用
suspend
标记,需要在内部调用其他suspend
函数真正切线程 - 按照代码书写顺序,线程自动切换
GlobalScope.launch (Dispatchers.Main){
}
上面代码会在主线程开启一个协程。
private suspend fun ioCode1(){
withContext(Dispatchers.IO){
Thread.sleep(1000)
Log.d(Companion.TAG, "onCreate:ioCode1=${Thread.currentThread().name} ")
}
}
上面函数用suspend
关键字修饰,并在函数内通过withContext(Dispatchers.IO)
切换到IO
线程执行。
private fun uiCode1(){
Log.d(Companion.TAG, "onCreate:uiCode1=${Thread.currentThread().name} ")
}
这是一个普通函数。
GlobalScope.launch (Dispatchers.Main){
ioCode1()
uiCode1()
}
把两个函数放在协程里执行:
01-21 14:17:49.960 10221-10256/com.example.coroutines D/MainActivity: onCreate:ioCode1=DefaultDispatcher-worker-2
01-21 14:17:49.963 10221-10221/com.example.coroutines D/MainActivity: onCreate:uiCode1=main
虽然ioCode1
是在io线程,但是ioCode1
和uiCode1
还是同步执行,如果ioCode1
方法体和uiCode1
一样,没有切换线程,那么编辑器会提示suspend
无用,也就是说,需要切换线程才需要suspend
关键字标记。
上面也回答了协程优势的问题,当你要执行耗时代码的时候,要用suspend
标记,执行的时候自动切换到对应切换的线程,执行完毕,线程又切回了当前开启协程的线程。
与 Retrofit 结合使用
Retrofit turns your HTTP API into a Java interface.
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
The
Retrofit
class generates an implementation of theGitHubService
interface.Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com/") .build(); GitHubService service = retrofit.create(GitHubService.class);
Each
Call
from the createdGitHubService
can make a synchronous or asynchronous HTTP request to the remote webserver.Call<List<Repo>> repos = service.listRepos("octocat");
上面是 Retrofit 官网的示例,我们也添加 Retrofit 依赖,完成上述代码:
添加依赖:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
声明 API :
interface GitHubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
}
data class Repo(
val name:String
)
请求:
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(GitHubService::class.java)
api.listRepos("JakeWharton")
.enqueue(object : Callback<List<Repo>> {
override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
Log.d(TAG, "onResponse: ")
}
override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
Log.d(TAG, "onFailure: ")
}
})
别忘了添加网络权限。
上面代码使用协程实现:
@GET("users/{user}/repos")
suspend fun listReposKt(@Path("user") user: String): List<Repo>
首先使用suspend
标记协程函数,然后去掉返回值的回调。
GlobalScope.launch(Dispatchers.Main) {
Log.d(TAG, "launch: ")
try {
val repos = api.listReposKt("JakeWharton")
Log.d(TAG, "listReposKt: ${repos[0].name}")
} catch (e: Exception) {
Log.d(TAG, "catch: ${e?.message}")
}
}
因为协程去掉了回调,所以需要 try catch 捕获异常。
使用 async 并发
有时候我们需要异步调用多个接口,然后在拿到所有结果后执行下一步,协程也可以轻松实现:
GlobalScope.launch(Dispatchers.Main) {
val one = async{api.listReposKt("JakeWharton")}
val two = async{api.listReposKt("JakeWharton")}
Log.d(TAG, "launch:${ one.await()[0].name}== ${ two.await()[0].name}")
}
在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于
launch
返回一个 Job 并且不附带任何结果值,而async
返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用.await()
在一个延期的值上得到它的最终结果, 但是Deferred
也是一个Job
,所以如果需要的话,你可以取消它。
协程泄漏
和线程一样,当退出activity
的时候,协程的后台线程还未执行完毕,那么就会发生内存泄漏。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch
的返回值是Job
,可以使用job.cancel()
取消协程。
job = GlobalScope.launch(Dispatchers.Main) {
ioCode1()
uiCode1()
}
override fun onDestroy() {
job?.cancel()
super.onDestroy()
}
CoroutineScope
如果一个页面有多个协程,需要取消多次,并不优雅,协程需要在协程作用域里执行,上面的GlobalScope
就是一个全局作用域,通常,我们需要声明一个 CoroutineScope ,onDestroy
的时候取消这个作用域,就可以取消所有运行其中的协程。
private val scope: CoroutineScope= MainScope()
启动协程:
scope.launch() {
ioCode1()
uiCode1()
}
取消:
override fun onDestroy() {
// job?.cancel()
scope.cancel()
super.onDestroy()
}
这样就可以启动多个协程,而取消一次。
将 Kotlin 协程与架构组件一起使用
上面例子通过 Kotlin 协程,我们可以定义 CoroutineScope
,管理运行协程的作用域和取消。
Android 的架构组件针对应用中的逻辑范围以及与 LiveData
的互操作层为协程提供了非常棒的支持。
生命周期感知型协程范围
在 lifecycle 组件中使用协程
在 lifecycle 组件中,比如一个activity
,我们可以直接使用协程,并不需要自己取消:
lifecycleScope.launch {
ioCode1()
uiCode1()
}
lifecycleScope
并不需要我们声明,扩展库提供的支持:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
在 ViewModel 组件中使用协程
在 ViewModel 组件中,我们一样可以方便的直接使用协程:
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
viewModelScope也不需要我们声明,扩展库提供的支持:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
将协程与 LiveData 一起使用
使用 LiveData
时,有时候可能需要异步计算值。可以使用 liveData
构建器函数调用 suspend
函数,并将结果作为 LiveData
对象传送。
在以下示例中,loadUser()
是在其他位置声明的协程函数。使用 liveData
构建器函数异步调用 loadUser()
,然后使用 emit()
发出结果:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
liveData
构建块用作协程和 LiveData
之间的结构化并发基元。当 LiveData
变为活动状态时,代码块开始执行;当 LiveData
变为非活动状态时,代码块会在可配置的超时过后自动取消。如果代码块在完成前取消,则会在 LiveData
再次变为活动状态后重启;如果在上次运行中成功完成,则不会重启。注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancellationException
)而取消,则不会重启。
协程的本质
协程怎么切换线程?
协程本质是对线程的上层封装,是线程切换,我们走读下源码。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
调到AbstractCoroutine
:
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
initParentJob()
start(block, receiver, this)
}
执行了start()
,这是一个operator
方法,CoroutineStart
:
@InternalCoroutinesApi
public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion)
ATOMIC -> block.startCoroutine(completion)
UNDISPATCHED -> block.startCoroutineUndispatched(completion)
LAZY -> Unit // will start lazily
}
Cancellable
:
@InternalCoroutinesApi
public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}
然后到DispatchedContinuation
:
@InternalCoroutinesApi
public fun <T> Continuation<T>.resumeCancellableWith(result: Result<T>): Unit = when (this) {
is DispatchedContinuation -> resumeCancellableWith(result)
else -> resumeWith(result)
}
@Suppress("NOTHING_TO_INLINE")
inline fun resumeCancellableWith(result: Result<T>) {
val state = result.toState()
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled()) {
resumeUndispatchedWith(result)
}
}
}
}
dispatcher.dispatch(context, this)
,这里应该都不陌生, Okhttp 和 Rxjava 里都出现过,调度线程用的,我们找到CoroutineDispatcher
的实现类HandlerDispatcher
,查看:
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}
关键代码就在这里,利用handler
把任务post
到对应的线程。
suspend
函数执行的本质
通过编译后的字节码我们看下ioCode1()
:
private suspend fun ioCode1() {
withContext(Dispatchers.IO) {
Thread.sleep(1000)
Log.d(TAG, "onCreate:ioCode1=${Thread.currentThread().name} ")
}
}
final Object ioCode1(Continuation $completion) {
Object var10000 = BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
Thread.sleep(1000L);
StringBuilder var10001 = (new StringBuilder()).append("onCreate:ioCode1=");
Thread var10002 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
return Boxing.boxInt(Log.d("MainActivity", var10001.append(var10002.getName()).append(' ').toString()));
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), $completion);
return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}
在协程里调用的时候,每个都对应一个label
,进入 Switch 判断执行。
协程挂起为什么不卡线程?
job = GlobalScope.launch(Dispatchers.Main) {
ioCode1()
uiCode1()
}
上面的协程是在主线程启动的,并且uiCode1
是没有切到子线程的,uiCode1
在ioCode1
之后执行,为什么主线程不会卡住?
其实没什么神奇,也是 Hanler 机制,执行完后会调用Handler().post { }
把任务 Post 到主线程。