Kotlin協(xié)程使用

什么是Kotlin協(xié)程

協(xié)程通過將復(fù)雜性放入庫來簡化異步編程。程序的邏輯可以在協(xié)程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關(guān)部分包裝為回調(diào)、訂閱相關(guān)事件、在不同線程(甚至不同機器)上調(diào)度執(zhí)行,而代碼則保持如同順序執(zhí)行一樣簡單。

協(xié)程就像非常輕量級的線程。線程是由系統(tǒng)調(diào)度的,線程切換或線程阻塞的開銷都比較大。而協(xié)程依賴于線程,但是協(xié)程掛起時不需要阻塞線程,幾乎是無代價的,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程,非常輕量級,一個線程中可以創(chuàng)建任意個協(xié)程。

協(xié)程雖然不能脫離線程而運行,但可以在不同的線程之間切換。


協(xié)程的優(yōu)勢:
引用庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
開啟協(xié)程
  1. runBlocking
    fun runBlocking() = runBlocking {
        repeat(5) {
            Log.i("minfo", "協(xié)程執(zhí)行$it,當前線程id:${Thread.currentThread().id}")
            delay(1000)
        }
    }

runBlocking啟動的協(xié)程任務(wù)會阻斷當前線程,直到該協(xié)程執(zhí)行結(jié)束。

2.launch

     fun runCoroutine() {
        Log.i("minfo", "協(xié)程開始")
        CoroutineScope(Dispatchers.Default).launch {
            delay(2000)
            Log.i("minfo", "協(xié)程內(nèi)部")
        }
        Log.i("minfo", "協(xié)程下面")
    }

launch啟動的協(xié)程不會阻斷當前線程,協(xié)程體后面執(zhí)行的代碼仍會繼續(xù)執(zhí)行,不會受協(xié)程開啟的影響。即我們主線程中的耗時操作可以開啟協(xié)程到子線程中進行處理。

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
}

launch() 是CoroutineScope的一個擴展函數(shù),CoroutineScope簡單來說就是協(xié)程的作用范圍。CoroutineScope.launch返回類型為Job類型。

取消協(xié)程

GlobalScope.launch返回對象job可直接調(diào)用cancel取消協(xié)程。

 job.cancel()
CoroutineScope

CoroutineScope即協(xié)程運行的作用域,主要作用是提供CoroutineContext,協(xié)程運行的上下文

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

我們常見的實現(xiàn)有GlobalScope,LifecycleScope,ViewModelScope等

協(xié)程下上文

協(xié)程是通過Dispatchers調(diào)度器來控制線程切換的。
上下文有一個重要的作用就是線程切換,Kotlin協(xié)程使用調(diào)度器來確定哪些線程用于協(xié)程執(zhí)行,Kotlin提供了調(diào)度器給我們使用:


掛起函數(shù)

協(xié)程體是一個用suspend關(guān)鍵字修飾的一個無參,無返回值的函數(shù)類型。被suspend修飾的函數(shù)稱為掛起函數(shù),與之對應(yīng)的是關(guān)鍵字resume。 Kotlin 的編譯器檢測到 suspend 關(guān)鍵字修飾的函數(shù)以后,會自動將掛起函數(shù)轉(zhuǎn)換成帶有 CallBack 的函數(shù),在做完異步事件后,會通過回調(diào)通知回去。而調(diào)用掛起函數(shù)的協(xié)程體后面的代碼,將不會繼續(xù)執(zhí)行,會等待掛起函數(shù)執(zhí)行完成,再繼續(xù)往下執(zhí)行。協(xié)程本身也是掛起函數(shù)。

那我們寫一個模擬切到子線程進行耗時操作:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //開啟一個協(xié)程,切換到主線程
        GlobalScope.launch(Dispatchers.Main) {
            tvContent.text = loadData()
        }
    }

    /**
     * 模擬訪問接口獲取接口數(shù)據(jù)
     */
    suspend fun loadData(): String {
        var response = ""
        withContext(Dispatchers.IO) {
            //子線程中模擬耗時操作
            for (i in 0..10000000) {

            }
            response = "response"
        }

        return response
    }

}

在launch開啟的協(xié)程體里,執(zhí)行l(wèi)oadData掛起方法,將協(xié)程掛起,協(xié)程中其后面的代碼不會執(zhí)行,只有等到loadData掛起結(jié)束恢復(fù)后才會執(zhí)行。

async/await用于獲取返回值處理:
    suspend fun loadData(): String {
        var response = ""
        withContext(Dispatchers.IO) {
            //子線程中模擬耗時操作
            for (i in 0..10000000) {

            }
            response = "response"
        }

        return response
    }

    suspend fun getResult(): String {
        var result = GlobalScope.async(Dispatchers.Main) {
            return@async loadData()
        }
        return result.await()
    }

        GlobalScope.launch(Dispatchers.Main) {
            Log.d("minfo", getResult())
        }
Retrofit結(jié)合協(xié)程請求網(wǎng)絡(luò)例子:

Retrofit從2.6.0開始已經(jīng)支持協(xié)程了,如果在網(wǎng)絡(luò)請求框架中,引入Rxjava僅僅用來與Retrofit結(jié)合使用,那就太沒有必要了。我們可以用協(xié)程來代替Rxjava這部分的功能。

這里是使用玩安卓的接口:https://www.wanandroid.com/banner/json

創(chuàng)建BaseResponse實體類基類:

open class BaseResponse<T> (var errorCode: Int = 0, var errorMsg: String = "", var data: T) : Serializable

在ApiService類中聲明一個請求函數(shù) ,聲明為掛起函數(shù),類型不需要添加Call。

interface ApiService {

    @GET("banner/json")
    suspend fun getFindData(): BaseResponse<List<DataBean>>
}

加入數(shù)據(jù)解析擴展函數(shù):

fun <T> BaseResponse<T>.dataConvert(): T {
    if (errorCode == 0) {
        return data
    } else {
        throw Exception(errorMsg)
    }
}
object RetrofitServiceManager {
    private val okHttpClient: OkHttpClient
    private val retrofit: Retrofit

    private const val DEFAULT_CONNECT_TIME = 10
    private const val DEFAULT_WRITE_TIME = 30
    private const val DEFAULT_READ_TIME = 30
    private const val REQUEST_PATH = "https://www.wanandroid.com/"

    init {

        okHttpClient = OkHttpClient.Builder()
            .connectTimeout(DEFAULT_CONNECT_TIME.toLong(), TimeUnit.SECONDS)//連接超時時間
            .writeTimeout(DEFAULT_WRITE_TIME.toLong(), TimeUnit.SECONDS)//設(shè)置寫操作超時時間
            .readTimeout(DEFAULT_READ_TIME.toLong(), TimeUnit.SECONDS)//設(shè)置讀操作超時時間
            .build()

        retrofit = Retrofit.Builder()
            .client(okHttpClient)//設(shè)置使用okhttp網(wǎng)絡(luò)請求
            .baseUrl(REQUEST_PATH)//設(shè)置服務(wù)器路徑
            .addConverterFactory(GsonConverterFactory.create())//添加轉(zhuǎn)化庫,默認是Gson
            .build()

    }

    fun <T> create(service: Class<T>): T {
        return retrofit.create(service)
    }
}

創(chuàng)建viewModel類,使用viewModelScope開啟協(xié)程:

class MyViewModel: ViewModel() {
    val items = ObservableArrayList<DataBean>()

    val itemBinding by lazy {
        ItemBinding.of<DataBean>(BR.item, R.layout.item).bindExtra(BR.viewModel, this)
    }

    init {
        getData()
    }

    private fun getData() {
        //viewModelScope是ViewModel的一個擴展函數(shù),也是一個協(xié)程
        viewModelScope.launch {
            try {
                var dataBean =  RetrofitServiceManager.create(ApiService::class.java).getFindData().dataConvert()
                items.addAll(dataBean)
            } catch (e: Exception) {
                e.printStackTrace()
                Log.i("TAG", "${e.message}")
            }
        }
    }
}

使用ViewModel:

        var binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel
請求結(jié)果并顯示在列表中:

使用viewModelScrope開啟協(xié)程,viewModel生命周期與UI組件生命周期一致,父協(xié)程結(jié)束,那么所有子協(xié)程都會結(jié)束,就能避免了子協(xié)程中內(nèi)存泄露的情況。

參考

Kotlin 協(xié)程 看這一篇就夠了
Kotlin協(xié)程

協(xié)程實現(xiàn)原理——狀態(tài)機

定義掛起函數(shù)

suspend fun requestLoadUser(id: String)
協(xié)程的掛起(suspend)和恢復(fù)(resume):

反編譯之后:

public final Object requestLoadUser(@NotNull String id, @NotNull Continuation $completion) {
      return "";
}

可以看到,多了個Continuation(繼續(xù))參數(shù),這是個接口,是在本次函數(shù)執(zhí)行完畢后執(zhí)行的回調(diào)。

public interface Continuation<in T> {
    /**
     * 保存上下文(比如變量狀態(tài))
     */
    public val context: CoroutineContext
    /**
     * 方法執(zhí)行結(jié)束的回調(diào),參數(shù)是個泛型,用來傳遞方法執(zhí)行的結(jié)果
     */
    public fun resumeWith(result: Result<T>)
}

suspend代碼示例:

suspend fun getToken(id: String): String = "token"
suspend fun getInfo(token: String): String = "info"

// 添加了局部變量a,看下suspend怎么保存a這個變量
suspend fun test() {
    val token = getToken("123") // 掛起點1,這里是異步線程
    var a = 10 // 這里是10  //主線程
    val info = getInfo(token) // 掛起點2,需要將前面的數(shù)據(jù)保存(比如a),在掛起點之后恢復(fù)   //異步線程
    println(info)  //主線程
    println(a
}

反編譯之后:

public final Object getToken(String id, Continuation completion) {
    return "token";
}

public final Object getInfo(String token, Continuation completion) {
    return "info";
}

// 重點函數(shù)(偽代碼)
public final Object test(Continuation<String>: continuation) {
    Continuation cont = new ContinuationImpl(continuation) {
        int label; // 保存狀態(tài)
        Object result; // 保存中間結(jié)果,還記得那個Result<T>嗎,是個泛型,因為泛型擦除,所以為Object,用到就強轉(zhuǎn)
        int tempA; // 保存上下文a的值,這個是根據(jù)具體代碼產(chǎn)生的
    };
    switch(cont.label) {
        case 0 : {
            cont.label = 1; //更新label
            
            getToken("123",cont) // 執(zhí)行對應(yīng)的操作,注意cont,就是傳入的回調(diào)
            break;
        }

        case 1 : {
            cont.label = 2; // 更新label
            
            // 這是一個掛起點,我們要保存上下文數(shù)據(jù),這里就保存a的值
            int a  = 10;
            cont.tempA = a; // 保存a的值 

            // 獲取上一步的結(jié)果,因為泛型擦除,需要強轉(zhuǎn)
            String token = (Object)cont.result;
            getInfo(token, cont); // 執(zhí)行對應(yīng)的操作
            break;
        }

        case 2 : {
            String info = (Object)cont.result; // 獲取上一步的結(jié)果
            println(info); // 執(zhí)行對應(yīng)的操作

            // 在掛起點之后,恢復(fù)a的值
            int a = cont.tempA;
            println(a);

            return;
        }
    }
}

我們可以將每個case理解為一個狀態(tài),每個case分支對應(yīng)的語句,理解為一個Continuation實現(xiàn)。
上述偽代碼大致描述了協(xié)程的調(diào)度流程:

1 調(diào)用test函數(shù)時,需要傳入一個Continuation接口,我們會對它進行二次裝飾。
2 裝飾就是根據(jù)函數(shù)具體邏輯,在內(nèi)部添加額外的上下文數(shù)據(jù)和狀態(tài)信息(也就是label)。
3 每個狀態(tài)對應(yīng)一個Continuation接口,里面會執(zhí)行對應(yīng)的業(yè)務(wù)邏輯。
4 每個狀態(tài)都會: 保存上下文信息 -> 獲取上一個狀態(tài)的結(jié)果 -> 執(zhí)行本狀態(tài)業(yè)務(wù)邏輯 -> 恢復(fù)上下文信息。
5 直到最后一個狀態(tài)對應(yīng)的邏輯執(zhí)行完畢。

總結(jié):

1 Kotlin中,每個suspend方法,都需要一個Continuation接口實現(xiàn),用來執(zhí)行下一個狀態(tài)的操作;并且,每個suspend方法的調(diào)用點都會產(chǎn)生一個掛起點。
2 每個掛起點,都會產(chǎn)生一個label,對應(yīng)于狀態(tài)機的一個狀態(tài),不同的狀態(tài)之間,通過Continuation來切換。
3 Kotlin協(xié)程會在每個掛起點保存當前的上下文數(shù)據(jù),并且在掛起點之后進行恢復(fù)。這樣,每個狀態(tài)之間就是相互獨立的,可以獨立調(diào)度。
4 協(xié)程的切換,只不過是從一種狀態(tài)切換到另一種狀態(tài),因為不同狀態(tài)是相互獨立的,所以在合適的時機,再切換回來也不會對結(jié)果造成影響。

參考:https://www.modb.pro/db/211852

https://mp.weixin.qq.com/s/70wBBKwFFLb0X_zrsvNzDA

https://www.bilibili.com/video/BV1KJ41137E9/?spm_id_from=333.337.search-card.all.click&vd_source=40c24e77b23dc2e50de2b7c87c6fed59

https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650257918&idx=1&sn=0bde010f58df22508907e213980eb1d6&chksm=88634891bf14c187ac0ad10562c26476a6caf531ded0a772e4bd29bfae23f2accbcebbd04668&scene=27

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

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