經過最近一年多閱讀文檔、使用及閱讀源碼的感覺:java
sleep()
,或者死鎖。new Thread()
新建線程的方式來比較性能消耗,來顯得更輕量化是不厚道的。我以爲應該與Executors.newCachedThreadPool()
來比才合適。實現一個從網絡請求用戶信息,而且把用戶暱稱顯示在UI上的功能: 實現第一個android
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//網絡請求,運行在後臺
textView.text = userInfo.name//更新UI,運行在主線程
}
suspend fun getUserFromNetwork(userId: String): String {
... //具體這裏的實現忽略,下文會分析到
}
複製代碼
從上面的協程示例中看到:api
getUserFromNetwork()
有suspend
這個修飾符GlobalScope.launch
使用了這個方法來啓動一個協程例如須要在一個列表中顯示,用戶的在線狀態及等級。bash
//須要經過網絡請求後臺api
api.getUserInfo(userId)//1.查詢用戶信息
api.getOnlineStatus(userId)//2.查詢在線狀態
api.getLevelInfo(userId)//3.查詢等級
複製代碼
3個api之間沒有依賴關係,最合理的方式應該是3個一塊兒併發請求再組裝數據。網絡
但在用回調時,要實現這樣的邏輯會變得很困難,權衡之下會把它寫成了在回調中串行執行,如此整個網絡的延時就會是原來的3倍。多線程
api.getUserInfo(userId, object : Callback<UserInfo> {
override fun onResponse(response: Response<UserInfo>) {
val userInfo = response.body
api.getOnlineStatus(userId, object: Callback<OnlineStatus> {
override fun onResponse(response: Response<OnlineStatus>) {
val onlineStatus = response.body
api.getLevelInfo(userId, object: Callback<LevelInfo> {
override fun onResponse(response: Response<LevelInfo>) {
val levelInfo = response.body
val composeInfo = compose(userInfo, onlineStatus, levelInfo)//組裝數據
getLiveData().postValue(composeInfo)//刷新UI
}
}
}
}
})
複製代碼
實現上述的業務邏輯,協程是怎麼作的?併發
GlobalScope.launch {
//async裏的3個block是同時請求,無需等待前一個的結果
val userInfo = async { api.getUserInfo(userId) }
val onlineStatus = async { api.getOnlineStatus(userId) }
val levelInfo = async { api.getLevelInfo(userId) }
//等待3個請求所有返回了,再組裝數據
val composeInfo = compose(userInfo.await(), onlineStatus.await(), levelInfo.await())
getLiveData().postValue(composeInfo)//刷新UI
}
複製代碼
咱們能夠用順序的方式來讓多線程執行起來,在同步和異步之間靈活的切換。這樣的特性,可讓咱們寫出以前很難才能作出的邏輯,這是Coroutine的優點。異步
async
來啓動一個新的協程@userInfo,返回一個Deferred
,調用Deferred.await()
方法,此時當前協程會掛起,等待@userInfo執行結束。runBlocking 上面已經講了2種啓動協程的方式,分別是
launch
,async
。然而還有一種叫runBlocking
的方式,在官方文檔也有寫到,同時也發現了會有同窗對這個方式存在一些使用上的誤解狀況。 當運行到這個runBlocking()
的時候當前線程會被阻塞住。特別注意這個方法不該在Coroutine內部使用。根據官方文檔說明,它是被用在阻塞main
線程及測試的時候使用的。不建議你們使用。async
runBlocking {
api.getUserFromNetwork(userId)
api.getOnlineStatus(userId)
}
...//直到getUserFromNetwork運行完纔會被執行到
複製代碼
這是一個第一個示例的全版:ide
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//網絡請求,運行在後臺
textView.text = userInfo.name//更新UI,運行在主線程
}
suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
HttpSerivce.getUser(userId)
}
複製代碼
Coroutine裏有一個withContext()
的函數,它能夠指定協程在哪一個線程裏執行,並讓後續代碼等待,按順序去執行。
有了這個函數,能夠消除在切換線程時致使的Callback嵌套。
若是咱們經過啓動不一樣的協程來切換線程,代碼是長這樣的:
GlobalScope.launch(Dispatchers.IO) {
...
launch(Dispatchers.Main) {
...
launch(Dispatchers.IO) {
...
}
}
}
複製代碼
是否是又有一種回調的感受回來了
而使用withContext()
則可讓協程擺脫上面的嵌套寫法。
GlobalScope.launch(Dispatchers.Main) {
val result0 = withContext(Dispatchers.IO) {...}
val result1 = withContext(Dispatchers.Main) {...}
val result2 = withContext(Dispatchers.IO) {...}
}
複製代碼
這裏須要跟前面併發請求的狀況區分開來,使用的場景不一樣進行選擇。
GlobalScope.launch(Dispatchers.Main) {
val userInfo = getUserFromNetwork(userId)//網絡請求,運行在後臺
textView.text = userInfo.name//更新UI,運行在主線程
}
suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
HttpSerivce.getUser(userId)
}
複製代碼
可見,對於suspendGetUserInfo()
內的邏輯運行在什麼線程裏,能夠不禁調用者決定的,能夠由實現者決定的。
咱們去設計本身的掛起函數時,若是須要在特定的線程裏,最好的方式是咱們函數內部去指定。好比操做文件讀寫的邏輯時,定義運行在
Dispatchers.IO
裏,這樣也不用擔憂外面會使用錯誤。
launch(Dispatchers.Main) {
val userInfo = suspendGetUserInfo(userId)
textView.text = userInfo.name
}
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
//可切換線程IO線程執行,原來執行的Main線程將空閒執行其它工做
return withContext(Dispatchers.IO) {
getUserFromNetwork(userId)
}
}
複製代碼
//在Main UI線程執行
log.debug("run in click starting")
GlobalScope.launch(Dispatchers.Main) {
log.debug("run in launch")
val userInfo = suspendGetUserInfo(userId)
}
log.debug("run in click finishing")
複製代碼
打印的順序是?
運行結果
D: 11:50:30.825 main: run in click starting
D: 11:50:30.869 main: run in click finishing
D: 11:50:30.872 main: run in launch
D: 11:50:30.873 main: run in suspendGetUserInfo starting
複製代碼
協程運行的時候,儘管仍是在Main裏運行,實際上也是在下個一個Main Looper時才運行到。
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
//可切換線程IO線程執行,原來執行的Main線程將空閒執行其它工做
log.debug("run in suspendGetUserInfo starting")
GlobalScope.launch(Dispatchers.Main) {
log.debug("Main is available")
}
val userInfo = withContext(Dispatchers.IO) {
delay(100)
log.debug("run in withContext")
getUserFromNetwork(userId)
}
log.debug("run in suspendGetUserInfo finishing")
return userInfo
}
複製代碼
打印的順序是?
D: 12:03:06.469 main : run in suspendGetUserInfo starting
D: 12:03:06.475 main : Main is available
D: 12:03:06.582 DefaultDispatcher-worker-2 : run in withContext
D: 12:03:06.585 main : run in suspendGetUserInfo finishing
複製代碼
因爲
withContext
是掛起函數,已經切換到IO上執行,所以Main是空閒的,下一個Looper的時候就能夠執行launch
裏的代碼。
suspend
方法的調用者必須是suspend
方法或者是在launch()/async()/runBlocking()
啓動的協程調用。suspend
的函數,當咱們要去使用時,要留意符合上面的規則。反之,沒有用到suspend
函數的地方,並不須要給本身函數加上,Android Studio也會相應的代碼提示。除了withContext()
,還有
await()
delay()
表示掛起必定時間後再運行,協程裏記得不要用sleep()
withTimeout { }
表示block執行若是超過必定的時間則會拋出TimeoutCancellationException
public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T
): T
複製代碼
public suspend fun delay(timeMillis: Long)
複製代碼
以前咱們開發的過程當中若是使用了Callback的形式來處理異步邏輯時,此時咱們想把Callback的API改成Coroutine要怎麼實現呢?
//原來的Callback形式方法
fun saveToRepositoryCallback(callback: (Int) -> Unit) { ... }
GlobalScope.launch(Dispatchers.IO) {
val returnValue = saveToRepository()
}
//協程的形式
suspend fun saveToRepository(): Int {
return suspendCoroutine { continuation ->
saveToRepositoryCallback {
continuation.resume(it)
}
}
}
複製代碼
經過一個回調的參數continuation
來設置返回的數據,此時就至關於把Callback改成了直接返回的形式。
以外若是須要Coroutine能拋出異常時,可使用resumeWithException
來把異常拋出來。
//原來的Callback形式方法,第二個參數爲當出錯返回的異常回調
fun saveToRepositoryCallback(success: (Int) -> Unit, error: (Throwable) -> Unit) {...}
GlobalScope.launch(Dispatchers.IO) {
try {
val returnValue = saveToRepository()
} catch (t: Throwable) {
// do something on error
}
}
//調用該方法有可能會throw exception
suspend fun saveToRepository(): Int {
return suspendCoroutine { continuation ->
saveToRepositoryCallback({
continuation.resume(it)
}, {
continuation.resumeWithException(it)
})
}
}
複製代碼
使用suspendCoroutine
啓動的Job
,想經過cancel()
來中止是不行的,依然會繼續執行
val job = GlobalScope.launch(Dispatchers.IO) {
val returnValue = saveToRepository()
LogUtil.debug("returnValue: $returnValue")
}
//協程的形式
suspend fun saveToRepository(): Int {
return suspendCoroutine { continuation ->
saveToRepositoryCallback {
LogUtil.debug("callback: $it")
continuation.resume(it)
}
}
}
//觸發取消
job.cancel()
I: [main] callback: 0
I: [DefaultDispatcher-worker-2] returnValue: 0
複製代碼
若是使用suspendCancellableCoroutine
,運行returnValue
的代碼則不會被運行到
val job = GlobalScope.launch(Dispatchers.IO) {
val returnValue = saveToRepository()
LogUtil.debug("returnValue: $returnValue")
}
suspend fun saveToRepository(): Int {
return suspendCancellableCoroutine { continuation ->
saveToRepositoryCallback {
LogUtil.debug("callback: $it")
continuation.resume(it)
}
}
}
I: [main] callback: 0
複製代碼
對於Kotlin Coroutine來講,更像是一個跨線程工具。能夠把它當作是相似於AsyncTask,Exeutors,Handler,Rxjava之類的工具庫。 碰到下面的狀況時建議使用協程:
如何添加Kotlin Coroutine的依賴:在build.gradle
中添加依賴庫。
buildscript {
ext {
kotlin_version = '1.3.50'
coroutines_android_version = '1.3.2'
}
}
dependencies {
//依賴kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//這是協程android的庫,同時也依賴了kotlin coroutine庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"
}
複製代碼