一些問題
程序什麼時候需要切線程?
- 工作比較耗時:放在後臺
- 工作特殊:需要放在指定線程(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 到主線程。