kotlin中的協(xié)程使用和原理

一些問題

程序什么時候需要切線程?

  • 工作比較耗時:放在后臺
  • 工作特殊:需要放在指定線程(ui刷新、計算、io)

kotlin的協(xié)程是什么?

  • 線程框架

  • 可以用同步代碼寫出異步操作

suspend 關鍵字是什么?

  • 并不是切線程關鍵字
  • 用于標記和提醒

協(xié)程的優(yōu)勢是什么?

  • 耗時代碼自動后臺,提高軟件性能

  • 線程執(zhí)行完畢自動切回起始線程

協(xié)程的使用

基本使用

  1. launch()開啟一段協(xié)程,一般需要指定Dispatchers.Main
  2. 把要在后臺工作的函數(shù),用suspend標記,需要在內(nèi)部調(diào)用其他suspend函數(shù)真正切線程
  3. 按照代碼書寫順序,線程自動切換
    GlobalScope.launch (Dispatchers.Main){
          
        }

上面代碼會在主線程開啟一個協(xié)程。

   private suspend fun ioCode1(){
       withContext(Dispatchers.IO){
                    Thread.sleep(1000)
           Log.d(Companion.TAG, "onCreate:ioCode1=${Thread.currentThread().name} ")
       }
    }

上面函數(shù)用suspend關鍵字修飾,并在函數(shù)內(nèi)通過withContext(Dispatchers.IO)切換到IO線程執(zhí)行。

    private  fun uiCode1(){
            Log.d(Companion.TAG, "onCreate:uiCode1=${Thread.currentThread().name} ")
    }

這是一個普通函數(shù)。

   GlobalScope.launch (Dispatchers.Main){
            ioCode1()
            uiCode1()
        }

把兩個函數(shù)放在協(xié)程里執(zhí)行:

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線程,但是ioCode1uiCode1還是同步執(zhí)行,如果ioCode1方法體和uiCode1一樣,沒有切換線程,那么編輯器會提示suspend無用,也就是說,需要切換線程才需要suspend關鍵字標記。

上面也回答了協(xié)程優(yōu)勢的問題,當你要執(zhí)行耗時代碼的時候,要用suspend標記,執(zhí)行的時候自動切換到對應切換的線程,執(zhí)行完畢,線程又切回了當前開啟協(xié)程的線程。

與 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 the GitHubService interface.

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.

Call<List<Repo>> repos = service.listRepos("octocat");

上面是 Retrofit 官網(wǎng)的示例,我們也添加 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: ")
                }
            })

別忘了添加網(wǎng)絡權限。

上面代碼使用協(xié)程實現(xiàn):

    @GET("users/{user}/repos")
    suspend fun listReposKt(@Path("user") user: String): List<Repo>

首先使用suspend標記協(xié)程函數(shù),然后去掉返回值的回調(diào)。

     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}")

            }

        }

因為協(xié)程去掉了回調(diào),所以需要 try catch 捕獲異常。

使用 async 并發(fā)

有時候我們需要異步調(diào)用多個接口,然后在拿到所有結果后執(zhí)行下一步,協(xié)程也可以輕松實現(xiàn):

        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。它啟動了一個單獨的協(xié)程,這是一個輕量級的線程并與其它所有的協(xié)程一起并發(fā)的工作。不同之處在于 launch 返回一個 Job 并且不附帶任何結果值,而 async 返回一個 Deferred —— 一個輕量級的非阻塞 future, 這代表了一個將會在稍后提供結果的 promise。你可以使用 .await() 在一個延期的值上得到它的最終結果, 但是 Deferred 也是一個 Job,所以如果需要的話,你可以取消它。

協(xié)程泄漏

和線程一樣,當退出activity的時候,協(xié)程的后臺線程還未執(zhí)行完畢,那么就會發(fā)生內(nèi)存泄漏。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job 

launch的返回值是Job,可以使用job.cancel()取消協(xié)程。

    job = GlobalScope.launch(Dispatchers.Main) {
            ioCode1()
            uiCode1()
        }
   override fun onDestroy() {
        job?.cancel()
        super.onDestroy()
    }

CoroutineScope

如果一個頁面有多個協(xié)程,需要取消多次,并不優(yōu)雅,協(xié)程需要在協(xié)程作用域里執(zhí)行,上面的GlobalScope就是一個全局作用域,通常,我們需要聲明一個 CoroutineScope ,onDestroy的時候取消這個作用域,就可以取消所有運行其中的協(xié)程。

private val scope: CoroutineScope= MainScope()

啟動協(xié)程:

     scope.launch() {
            ioCode1()
            uiCode1()
        }

取消:

    override fun onDestroy() {
//        job?.cancel()
        scope.cancel()
        super.onDestroy()
    }

這樣就可以啟動多個協(xié)程,而取消一次。

將 Kotlin 協(xié)程與架構組件一起使用

上面例子通過 Kotlin 協(xié)程,我們可以定義 CoroutineScope,管理運行協(xié)程的作用域和取消。

Android 的架構組件針對應用中的邏輯范圍以及與 LiveData 的互操作層為協(xié)程提供了非常棒的支持。

生命周期感知型協(xié)程范圍

在 lifecycle 組件中使用協(xié)程

在 lifecycle 組件中,比如一個activity,我們可以直接使用協(xié)程,并不需要自己取消:

    lifecycleScope.launch {
                ioCode1()
                uiCode1()
            }

lifecycleScope 并不需要我們聲明,擴展庫提供的支持:

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

在 ViewModel 組件中使用協(xié)程

在 ViewModel 組件中,我們一樣可以方便的直接使用協(xié)程:

    viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }

viewModelScope也不需要我們聲明,擴展庫提供的支持:

    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

將協(xié)程與 LiveData 一起使用

使用 LiveData 時,有時候可能需要異步計算值??梢允褂?liveData 構建器函數(shù)調(diào)用 suspend 函數(shù),并將結果作為 LiveData 對象傳送。

在以下示例中,loadUser() 是在其他位置聲明的協(xié)程函數(shù)。使用 liveData 構建器函數(shù)異步調(diào)用 loadUser(),然后使用 emit() 發(fā)出結果:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

liveData 構建塊用作協(xié)程和 LiveData 之間的結構化并發(fā)基元。當 LiveData 變?yōu)榛顒訝顟B(tài)時,代碼塊開始執(zhí)行;當 LiveData 變?yōu)榉腔顒訝顟B(tài)時,代碼塊會在可配置的超時過后自動取消。如果代碼塊在完成前取消,則會在 LiveData 再次變?yōu)榛顒訝顟B(tài)后重啟;如果在上次運行中成功完成,則不會重啟。注意,代碼塊只有在自動取消的情況下才會重啟。如果代碼塊由于任何其他原因(例如,拋出 CancellationException)而取消,則不會重啟。

協(xié)程的本質(zhì)

協(xié)程怎么切換線程?

協(xié)程本質(zhì)是對線程的上層封裝,是線程切換,我們走讀下源碼。

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
}

調(diào)到AbstractCoroutine

   public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        initParentJob()
        start(block, receiver, this)
    }

執(zhí)行了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 里都出現(xiàn)過,調(diào)度線程用的,我們找到CoroutineDispatcher的實現(xiàn)類HandlerDispatcher,查看:

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }

關鍵代碼就在這里,利用handler把任務post到對應的線程。

suspend函數(shù)執(zhí)行的本質(zhì)

通過編譯后的字節(jié)碼我們看下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;
   }

在協(xié)程里調(diào)用的時候,每個都對應一個label,進入 Switch 判斷執(zhí)行。

協(xié)程掛起為什么不卡線程?

        job = GlobalScope.launch(Dispatchers.Main) {
            ioCode1()
            uiCode1()
        }

上面的協(xié)程是在主線程啟動的,并且uiCode1是沒有切到子線程的,uiCode1ioCode1之后執(zhí)行,為什么主線程不會卡???

其實沒什么神奇,也是 Hanler 機制,執(zhí)行完后會調(diào)用Handler().post { }把任務 Post 到主線程。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 在今年的三月份,我因為需要為項目搭建一個新的網(wǎng)絡請求框架開始接觸 Kotlin 協(xié)程。那時我司項目中同時存在著兩種...
    Android開發(fā)指南閱讀 1,002評論 0 2
  • Why 簡化異步代碼的編寫。 執(zhí)行嚴格主線程安全確保你的代碼永遠不會意外阻塞主線程,并增強了代碼的可讀性。 提升代...
    熹哥閱讀 676評論 0 3
  • 本文主要介紹協(xié)程的用法, 以及使用協(xié)程能帶來什么好處. 另外, 也會粗略提一下協(xié)程的大致原理.本文的意義可能僅僅是...
    登高而望遠閱讀 35,531評論 18 140
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學鈴聲...
    飄雪兒5閱讀 7,818評論 16 22
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    余生動聽閱讀 10,836評論 0 11

友情鏈接更多精彩內(nèi)容