這裏一開始不打算介紹什麼是協程,雖然標題叫介紹~~html
爲了方便理解,這邊先作個比喻: 從使用的角度來看,Kotlin的協程像是「另外一種RxJava」,可是比RxJava要高效。java
這裏先有個大體的印象,先了解下協程在實際中的做用,回頭再去看它的原理,或許會更容易些。android
一開始查了好多關於協程資料(包括官方完檔),發現不一樣的人說的不大同樣,最後越看越亂。因而我決定一開始先不說什麼是協程。後端
上面說到,協程用起來「像是另外一種RxJava」。api
那麼是否是能夠用協程來開啓一個異步操做?切換線程? 答案是確定的,不只能夠作到,並且寫起來也很簡單。下面看個栗子網絡
舉個例子,這裏有個登陸操做,須要用兩個接口才能完成。 一、使用帳號密碼去獲取token 二、經過token獲取用戶信息異步
很明顯,這是個嵌套的請求。代碼立刻就浮如今腦海中,因而咱們埋頭「papapa」,很快就寫出了這樣的一段:async
reqToken(new CallBack<String>() { //請求token
@Override
public void onSuccess(String token) {
reqUserInfo(token, new CallBack<UserInfo>() { //經過token,獲取用戶信息
@Override
public void onSuccess(UserInfo userInfo) {
Logger.Companion.i("login success");
}
});
}
});
複製代碼
是的,確實沒什麼問題。不過沒以爲這要的代碼很長嗎?ide
因而咱們改用lambda簡寫,或是kotlin:函數
reqToken{ //請求token
reqUserInfo(it){ //經過token,獲取用戶信息
Logger.i("login success")
}
}
複製代碼
nice,瞬間簡潔了好多
確實簡潔了不少。不過仍是難逃嵌套結構,若是多來幾層,最後可能成了這樣:
看得頭皮發麻~~
可是!!!,若果用協程就不同了(劃重點)
coroutineScope.launch {
val token = getToken()
val userInfo = getUserInfo(token)
Logger.i("login success")
}
複製代碼
不只代碼少,並且能夠用同步的方式來寫異步!!!
往下再瞭解一點?
知道到了他的優(niu)秀(bi)之處,下面來看看是怎麼用的
由於是Kotlin的協程,因此項目須要支持Kotlin。怎麼支持就不用我說了吧? (不要問我,我不會,由於那是另外一個同事作的。hahaha~~~)
gradle倒入協程依賴
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
複製代碼
能夠直接new一個MainScope
val mainScop = MainScope()
複製代碼
注意記得在銷燬的時候調用cancel()
,調用cancel()
後用mainScope
啓動的協程都會取消掉。
override fun onDestroy() {
super.onDestroy()
mainScope.cancel()
}
複製代碼
經常使用的方式有兩種:launch()
、async()
,下面分別來講明他們的用途。
固然還有其餘的建立方式,這裏就不說了。
使用launch()
建立一個協程,返回一個Job
對象。
val job = mainScope.launch {
//在協程中的操做
Logger.i("launch end")
}
複製代碼
很簡單,mainScope.launch{}
就能建立一個協程,大括號中的代碼是在協程中執行的。
看下打印的日誌,發現這個協程時在主線程中運行的。
"這有什麼用?在主線程中運行的協程?那我再裏面作耗時操做,是否是會卡住?"
確實,若是直接這樣用時會阻塞主線程的。因此這時候,就須要用到withContext()
這裏用的是上面的
mainScope
,這個做用域內的調度器是基於主線程調度器的。也就是說,mainScope.launch()
獲得的協程默認都是在主線程中。因此println("in main scope")
是在主線程中運行的
withContext()
:**用給定的協程上下文調用指定的暫停塊,暫停直到完成,而後返回結果。**也就是說,能夠用來切換線程,並返回執行後的結果。
經常使用的有 Dispatchers.Main
:工做在主線程中 Dispatchers.Default
:將會獲取默認調度器(子線程) Dispatchers.IO
:IO線程 Dispatchers.Unconfined
:是一個特殊的調度器(說實話,我沒搞懂他的用法~~)
這裏子線程中請求一個token,而後回到主線程中:
mainScope.launch {
val token = withContext(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()//注意!!!這是個同步的網絡請求
token
}
Logger.i("token $token")
}
複製代碼
再來看下日誌
有withContext()後,線程的切換顯得是那麼簡單。只要你開心,能夠切來切去。
mainScope.launch {
withContext(Dispatchers.Default) {
Logger.i("切到子線程")
}
withContext(Dispatchers.Main) {
Logger.i("切到主線程")
}
withContext(Dispatchers.IO) {
Logger.i("切到IO線程")
}
Logger.i("launch end")
}
複製代碼
除了launch()
,還有個經常使用的方法——async()
,async()
和launch()
類似。不一樣的是他能夠返回協程執行結束後值。
async()
返回的是一個Deferred
對象,須要經過Deferred#await()
獲得返回值。
仍是上面的例子:子線程中請求一個token,而後回到主線程中:
mainScope.launch {
val tokenDeferred = async(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()//注意!!!這是個同步的網絡請求
token //返回token
}
val token = tokenDeferred.await()
Logger.i("token : $token")
}
複製代碼
打印的結果上面同樣,就不貼圖了。
async()
和launch()
同樣,都能指定執行的線程。
因爲
Deferred#await()
須要在協程中調用,因此上面在launch()
中使用async()
。
「這有什麼用?跟launch()差很少啊?」
額~~ 用處大了,往下看
若是切換線程中的代碼不少,想把(withContext(){...}
)的代碼抽出來。因而寫成這樣
fun getToken(): String {
return withContext(Dispatchers.Default) {
//同步請求獲得token
val token = api.getToken()
token
}
}
複製代碼
BUT,並不能這樣用,發現編譯器報錯了:
withContext()
只能在
協程或**
suspend
**方法中使用。因此,在方法前加上
suspend
就不會報錯了。
suspend fun getToken(): String { ... }
複製代碼
suspend:申明這是個可掛起的函數,裏面能夠用協程的一下方法(launch()、async()、withContext()等)。
有了協程,寫異步的代碼將會方便不少。
回到一開始的栗子,請求token,而後用token請求UserInfo
mainScope.launch {
//獲取token
val token = withContext(Dispatchers.Default) {
val token = api.getToken()
token
}
//經過token,獲取userInfo
val userInfo = withContext(Dispatchers.Default) {
val userInfo = api.getUserInfo(token)
userInfo
}
//登陸成功
Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
複製代碼
看到這裏,你可能會:「不對啊,一開始的栗子沒這麼複雜~~~」
由於上面的例子中,把請求那部分的代碼抽到suspend
方法去了。
mainScope.launch {
//獲取token
val token = getToken()
//經過token,獲取userInfo
val userInfo = getUserInfo(token)
//登陸成功
Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
---------------------------------------
suspend fun getUserInfo(token: String): UserInfo {
return withContext(Dispatchers.Default) {
Logger.i("get userInfo, token: $token")
val userInfo = api.getUserInfo(token)
userInfo
}
}
suspend fun getToken(): String {
return withContext(Dispatchers.Default) {
Logger.i("get token")
val token = api.getToken()
token
}
複製代碼
稍微調整下,就會發現和上面是栗子是同樣的
有時候,遇到「優秀」的後端同窗。一個頁面須要請求兩個接口,用兩個接口返回的數據才能渲染出頁面。
這裏發起兩個連續的請求也能夠作到,可是若是能夠變成兩個並行的請求,豈不美哉?
那麼,async()
就能夠排上用場了。
mainScope.launch {
val timeMillis = measureTimeMillis { //記錄耗時
val deferred1 = async { getData1() }
val deferred2 = async { getData2() }
val data1 = deferred1.await()
val data2 = deferred2.await()
Logger.i("data1: $data1, data1: $data2")
}
Logger.i("timeMillis : $timeMillis")
}
--------------------------------------------------
suspend fun getData1(): String {
return withContext(Dispatchers.Default) {
Thread.sleep(1000)
"value1"
}
}
suspend fun getData2(): String {
return withContext(Dispatchers.Default) {
Thread.sleep(1000)
"value2"
}
}
複製代碼
查看日誌
會發現,getData2()
和 getData1()
都是延遲1000ms的請求,若是用串行的方式來寫,耗時確定超過2000ms。使用async()
耗時也才1051ms。
這裏用
measureTimeMillis()
來計算代碼耗時。
協程基本的使用到這裏就能夠告一段落了,主要介紹了協程給我帶來了什麼,能夠在什麼場景下用,怎麼用。相信這樣同步的方式來寫異步,這樣寫出來的代碼必定是很是直觀、清晰的。
然而,有關什麼是協程?有哪些詳細的用法和細節?進程、線程和協程又有什麼關係?留着後面後面再說。
Kotlin 官網 Kotlin Coroutines(協程) 徹底解析 Kotlin Primer·第七章·協程庫 探究高級的Kotlin Coroutines知識 破解 Kotlin 協程
以上有錯誤之處,感謝指出