什么是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é)程
- 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