先不講概念寒屯,先上代碼,看一下協(xié)程怎么用的黍少。
retrofit 請(qǐng)求代碼
interface HttpInterface {
@GET("/photos/random")
suspend fun getImageRandom(@Query("count") count: Number): ArrayList<ImageBean>
}
activity 中調(diào)用代碼
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
val imageArray: ArrayList<ImageBean> = httpInterface.getImageRandom(10)//發(fā)送請(qǐng)求
textView.text = "圖片數(shù)量為" + imageArray.size//更新UI
}
}
可以看到發(fā)送請(qǐng)求和更新 UI 在一個(gè)代碼塊中寡夹,看起來(lái)像是都運(yùn)行在主線程中,但是竟然沒有任何報(bào)錯(cuò)厂置。 這就是協(xié)程最有魅力的地方非阻塞式掛起
菩掏,后邊會(huì)詳細(xì)介紹。
創(chuàng)建協(xié)程
創(chuàng)建協(xié)程有三種方式:launch昵济、async智绸、runBlocking
launch
launch 方法簽名如下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//省略
return coroutine
}
launch 是 CoroutineScope 的擴(kuò)展方法野揪,需要 3 個(gè)參數(shù)。第一個(gè)參數(shù)瞧栗,看字面意思是協(xié)程上下文斯稳,后邊會(huì)重點(diǎn)講到。第二個(gè)參數(shù)是協(xié)程啟動(dòng)模式沼溜,默認(rèn)情況下平挑,協(xié)程是創(chuàng)建后立即執(zhí)行的。第三個(gè)參數(shù)系草,官方文檔說這個(gè) block 就是協(xié)程代碼塊,所以是必傳的唆涝。返回的是一個(gè) Job找都,這個(gè) Job 可以理解為一個(gè)后臺(tái)工作,在 block 代碼塊執(zhí)行完成后會(huì)結(jié)束廊酣,也可以通過 Job 的 cancel 方法取消它能耻。
async
async 方法簽名如下:
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
//省略
return coroutine
}
同樣也是 CoroutineScope 的擴(kuò)展方法,參數(shù)跟 launch 是一模一樣的亡驰,只是返回參數(shù)變成了 Deferred晓猛,這個(gè) Deferred 繼承于 Job,相當(dāng)于一個(gè)帶返回結(jié)果的 Job凡辱,返回結(jié)果可以通過調(diào)用它的 await 方法獲取戒职。
runBlocking
runBlocking 會(huì)阻塞調(diào)用他的線程,直到代碼塊執(zhí)行完畢透乾。
Log.i("zx", "當(dāng)前線程1-" + Thread.currentThread().name)
runBlocking(Dispatchers.IO) {
delay(2000)
Log.i("zx", "休眠2000毫秒后洪燥,當(dāng)前線程" + Thread.currentThread().name)
}
Log.i("zx", "當(dāng)前線程2-" + Thread.currentThread().name)
輸出內(nèi)容
當(dāng)前線程1-main
休眠2000毫秒后,當(dāng)前線程DefaultDispatcher-worker-1
當(dāng)前線程2-main
可以看到乳乌,即使協(xié)程指定了運(yùn)行在 IO 線程捧韵,依舊會(huì)阻塞主線程。runBlocking 主要用來(lái)寫測(cè)試代碼汉操,平常不要隨意用再来,所以不再過多介紹。
CoroutineScope 協(xié)程作用域
launch 和 async 都是 CoroutineScope 的擴(kuò)展函數(shù)磷瘤,CoroutineScope 又是什么呢芒篷,字面意思翻譯過來(lái)是協(xié)程作用域,協(xié)程作用域類似于變量作用域膀斋,定義了協(xié)程代碼的作用范圍梭伐。作用域取消時(shí),作用域中的協(xié)程都會(huì)被取消仰担。 比如如下代碼:
MainScope().launch {
var i = 0
launch(Dispatchers.IO) {
while (true) {
Log.i("zx", "子協(xié)程正在運(yùn)行著$i")
delay(1000)
}
}
while (true) {
i++
Log.i("zx", "父協(xié)程正在運(yùn)行著$i")
if (i>4) {
cancel()
}
delay(1000)
}
}
輸出:
父協(xié)程正在運(yùn)行著1
子協(xié)程正在運(yùn)行著1
父協(xié)程正在運(yùn)行著2
子協(xié)程正在運(yùn)行著2
父協(xié)程正在運(yùn)行著3
子協(xié)程正在運(yùn)行著3
父協(xié)程正在運(yùn)行著4
子協(xié)程正在運(yùn)行著4
子協(xié)程正在運(yùn)行著4
父協(xié)程正在運(yùn)行著5
5 秒后糊识,父協(xié)程調(diào)用 cancel()結(jié)束了绩社,子協(xié)程也就結(jié)束了,并沒有繼續(xù)打印出值。
可以通過 CoroutineScope()來(lái)創(chuàng)建協(xié)程作用域赂苗,這并不是一個(gè)構(gòu)造函數(shù)愉耙,CoroutineScope 是一個(gè)接口,所以沒有構(gòu)造函數(shù)拌滋,只是函數(shù)名與接口名同名而已朴沿,源碼如下:
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
源碼可見,創(chuàng)建 CoroutineScope 時(shí)需要傳入 CoroutineContext败砂,這個(gè) CoroutineContext 也是 CoroutineScope 接口中唯一的成員變量赌渣。CoroutineScope.kt 這個(gè)文件中使用 CoroutineScope()創(chuàng)建了兩個(gè) Scope,一個(gè)是 MainScope昌犹,一個(gè)是 GlobalScope坚芜。源碼如下:
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
MainScope 是一個(gè)方法,返回了一個(gè)運(yùn)行在主線程的作用域斜姥,需要手動(dòng)取消鸿竖。GlobalScope 是一個(gè)全局作用域,整個(gè)應(yīng)用程序生命周期他都在運(yùn)行铸敏,不能提前取消缚忧,所以一般不會(huì)使用這個(gè)作用域。Android 中杈笔,ktx 庫(kù)提供了一些常用的作用域供我們使用闪水,如 lifecycleScope 和 viewModelScope。在 LifecycleOwner 的所有實(shí)現(xiàn)類中桩撮,如 Activity 和 Fragment 中都可以直接使用 lifecycleScope敦第,lifecycleScope 會(huì)跟隨 Activity 或 Fragment 的生命周期,在 Activity 或 Fragment 銷毀時(shí)店量,自動(dòng)取消協(xié)程作用域中的所有協(xié)程芜果,不用手動(dòng)管理,不存在內(nèi)存泄露風(fēng)險(xiǎn)融师。類似的 viewModelScope 也會(huì)隨著 viewModel 的銷毀而取消右钾。
目前已經(jīng)有好幾個(gè)地方出現(xiàn)了 CoroutineContext:?jiǎn)?dòng)協(xié)程時(shí) launch 或者 async 方法需要 CoroutineContext,創(chuàng)建協(xié)程作用域時(shí)需要 CoroutineContext旱爆,協(xié)程作用域中有且只有一個(gè)成員變量也是 CoroutineContext舀射,如下源碼所示:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
如此看來(lái),CoroutineContext 必定很重要怀伦。
CoroutineContext 協(xié)程上下文
CoroutineContext 保存了協(xié)程的上下文脆烟,是一些元素的集合(實(shí)際并不是用集合 Set 去存儲(chǔ)),集合中每一個(gè)元素都有一個(gè)唯一的 key房待。通俗來(lái)講邢羔,CoroutineContext 保存了協(xié)程所依賴的各種設(shè)置驼抹,比如調(diào)度器、名稱拜鹤、異常處理器等等框冀。
CoroutineContext 源碼如下:
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else
context.fold(this) { acc, element ->
//省略
}
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
//省略
}
}
CoroutineContext 里有一個(gè)接口 Element,這個(gè) Element 就是組成 CoroutineContext 的元素敏簿,最重要的是 plus 操作符函數(shù)明也,這個(gè)函數(shù)可以把幾個(gè) Element 合并成為一個(gè) CoroutineContext,由于是操作符函數(shù)惯裕,所以可以直接用+
調(diào)用温数。比如:
var ctx = Dispatchers.IO + Job() + CoroutineName("測(cè)試名稱")
Log.i("zx", ctx.toString())
輸出
[JobImpl{Active}@31226a0, CoroutineName(測(cè)試名稱), Dispatchers.IO]
共有哪幾種元素呢?來(lái)看看 Element 的子類吧轻猖。Element 有這么幾個(gè)子類(子接口):Job帆吻、CoroutineDispatcher、CoroutineName咙边、CoroutineExceptionHandler。
Job
Job 可以簡(jiǎn)單理解為一個(gè)協(xié)程的引用次员,創(chuàng)建協(xié)程后會(huì)返回 Job 實(shí)例败许,可以通過 Job 來(lái)管理協(xié)程的生命周期。Job 是 CoroutineContext 元素的一種淑蔚,可以傳入 CoroutineScope 用來(lái)使協(xié)程有不同的特性市殷。主要關(guān)注Job()
、SupervisorJob()
這兩個(gè)創(chuàng)建 Job 的函數(shù)以及Deferred
這個(gè) Job 的子接口刹衫。
Job()
創(chuàng)建一個(gè)處于活動(dòng)狀態(tài)的 Job 對(duì)象醋寝,可以傳入父 Job,這樣當(dāng)父 Job 取消時(shí)就可以取消該 Job 以及他的子項(xiàng)带迟。 該 Job 的任何子項(xiàng)失敗都會(huì)立即導(dǎo)致該 Job 失敗音羞,并取消其其余子項(xiàng)。這個(gè)很好理解仓犬,例如:
CoroutineScope(Dispatchers.IO + Job()+MyExceptionHandler()).launch {
var index = 0
launch {
while (true) {
index++
if (index > 3) {
throw Exception("子協(xié)程1異常了")
}
Log.i("zx", "子協(xié)程1正在運(yùn)行")
}
}
launch {
while (true) {
Log.i("zx", "子協(xié)程2正在運(yùn)行")
}
}
}
子協(xié)程 1 異常了嗅绰,就會(huì)導(dǎo)致整個(gè) Job 失敗,子協(xié)程 2 也不會(huì)繼續(xù)運(yùn)行搀继。
SupervisorJob()
創(chuàng)建一個(gè)處于活動(dòng)狀態(tài)的 Job 對(duì)象窘面。 該 Job 的子項(xiàng)之間彼此獨(dú)立,互不影響叽躯,子項(xiàng)的失敗或取消不會(huì)導(dǎo)致主 Job 失敗财边,也不會(huì)影響其他子項(xiàng)。
CoroutineScope(Dispatchers.IO + SupervisorJob() + MyExceptionHandler()).launch {
launch {
while (true) {
index++
if (index > 3) {
throw Exception("子協(xié)程1異常了")
}
Log.i("zx", "子協(xié)程1正在運(yùn)行")
}
}
launch {
while (true) {
Log.i("zx", "子協(xié)程2正在運(yùn)行")
}
}
}
同樣的代碼点骑,把 Job()換成 SupervisorJob()后酣难,可以發(fā)現(xiàn)子協(xié)程 2 會(huì)一直運(yùn)行谍夭,并不會(huì)因?yàn)樽訁f(xié)程 1 異常而被取消。
我們常見的 MainScope鲸鹦、viewModelScope慧库、lifecycleScope 都是用 SupervisorJob()創(chuàng)建的,所以這些作用域中的子協(xié)程異常不會(huì)導(dǎo)致根協(xié)程退出馋嗜。 kotlin 提供了一個(gè)快捷函數(shù)創(chuàng)建一個(gè)使用 SupervisorJob 的協(xié)程齐板,那就是 supervisorScope。例如:
CoroutineScope(Dispatchers.IO).launch {
supervisorScope {
//這里的子協(xié)程代碼異常不會(huì)導(dǎo)致父協(xié)程退出葛菇。
}
}
等同于
CoroutineScope(Dispatchers.IO).launch {
launch(SupervisorJob()) {
}
}
Deferred
是 Job 的子接口甘磨,是一個(gè)帶有返回結(jié)果的 Job。async 函數(shù)創(chuàng)建的協(xié)程會(huì)返回一個(gè) Deferred眯停,可以通過 Deferred 的await()
獲取實(shí)際的返回值济舆。async 與 await 類似于其他語(yǔ)言(例如 JavaScript)中的 async 與 await,通常用來(lái)使兩個(gè)協(xié)程并行執(zhí)行莺债。 例如如下代碼
suspend fun testAsync1(): String = withContext(Dispatchers.Default)
{
delay(2000)
"123"
}
suspend fun testAsync2(): String = withContext(Dispatchers.Default)
{
delay(2000)
"456"
}
lifecycleScope.launch {
val time1 = Date()
val result1 = testAsync1()
val result2 = testAsync2()
Log.i("zx", "結(jié)果為${result1 + result2}")
Log.i("zx", "耗時(shí)${Date().time - time1.time}")
}
會(huì)輸出:
結(jié)果為123456
耗時(shí)5034
如果改為使用 async滋觉,讓兩個(gè)協(xié)程并行。代碼如下:
lifecycleScope.launch {
val time1 = Date()
val result1 = async { testAsync1() }
val result2 = async { testAsync2() }
Log.i("zx", "結(jié)果為${result1.await() + result2.await()}")
Log.i("zx", "耗時(shí)${Date().time - time1.time}")
}
輸出
結(jié)果為123456
耗時(shí)3023
總耗時(shí)為兩個(gè)并行協(xié)程中耗時(shí)較長(zhǎng)的那個(gè)時(shí)間齐邦。
CoroutineDispatcher 調(diào)度器
指定了協(xié)程運(yùn)行的線程或線程池椎侠,共有 4 種。
- Dispatchers.Main 運(yùn)行在主線程措拇,Android 平臺(tái)就是 UI 線程我纪,是單線程的。
- Dispatchers.Default 默認(rèn)的調(diào)度器丐吓,如果上下文中未指定調(diào)度器浅悉,那么就是 Default。適合用來(lái)執(zhí)行消耗 CPU 資源的計(jì)算密集型任務(wù)券犁。它由 JVM 上的共享線程池支持术健。 默認(rèn)情況下,此調(diào)度器使用的最大并行線程數(shù)等于 CPU 內(nèi)核數(shù)族操,但至少為兩個(gè)苛坚。
- Dispatchers.IO IO 調(diào)度器,使用按需創(chuàng)建的線程共享池色难,適合用來(lái)執(zhí)行 IO 密集型阻塞操作泼舱,比如 http 請(qǐng)求。此調(diào)度器默認(rèn)并行線程數(shù)為內(nèi)核數(shù)和 64 這兩個(gè)值中的較大者枷莉。
- Dispatchers.Unconfined 不限于任何特定線程的協(xié)程調(diào)度器娇昙,不常用。
需要注意的是 Default 和 IO 都是運(yùn)行在線程池中笤妙,兩個(gè)子協(xié)程有可能是在一個(gè)線程中冒掌,有可能不是一個(gè)線程中噪裕。例如如下代碼:
CoroutineScope(Dispatchers.IO).launch {
launch {
delay(3000)
Log.i("zx", "當(dāng)前線程1-" + Thread.currentThread().name)
}
launch {
Log.i("zx", "當(dāng)前線程2-" + Thread.currentThread().name)
}
}
輸出
當(dāng)前線程2-DefaultDispatcher-worker-2
當(dāng)前線程1-DefaultDispatcher-worker-5
所以,如果涉及線程的 ThreadLocal 數(shù)據(jù)時(shí)股毫,記得做處理膳音。
如果一不小心用錯(cuò)了 Dispatchers.Default 去發(fā) IO 請(qǐng)求會(huì)有什么后果呢?猜測(cè)結(jié)果:由于 Default 調(diào)度器并行線程數(shù)遠(yuǎn)小于 IO 調(diào)度器铃诬,IO 請(qǐng)求的一個(gè)特性就是等待時(shí)間很長(zhǎng)祭陷,而實(shí)際的處理時(shí)間很短,所以會(huì)造成大量請(qǐng)求處于等待分配線程的狀態(tài)中趣席,造成效率低下兵志。實(shí)際情況可以寫個(gè)程序測(cè)試一下,這里就不試了宣肚。
CoroutineName 協(xié)程名稱
傳入一個(gè) String 作為協(xié)程名稱想罕,一般用于調(diào)試時(shí)日志輸出,以區(qū)分不同的調(diào)度器霉涨。
CoroutineExceptionHandler 異常處理器
用于處理協(xié)程作用域內(nèi)所有未捕獲的異常按价。實(shí)現(xiàn) CoroutineExceptionHandler 接口就好了,代碼如下:
class MyExceptionHandler : CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*>
get() = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, exception: Throwable) {
Log.i("zx", "${context[CoroutineName]}中發(fā)生異常,${exception.message}")
}
}
然后用+
拼接并設(shè)置給作用域笙瑟。
CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
launch(CoroutineName("子協(xié)程1") + MyExceptionHandler()) {
throw Exception("完蛋了俘枫,異常了")
}
}
輸出內(nèi)容為
CoroutineName(父協(xié)程)中發(fā)生異常,完蛋了,異常了
不對(duì)呀逮走,明明是子協(xié)程 1 拋出的異常,為什么輸出的是父協(xié)程拋出的異常呢今阳?原來(lái)师溅,異常規(guī)則就是子協(xié)程會(huì)將異常一級(jí)一級(jí)向上拋,直到根協(xié)程
盾舌。那什么是根協(xié)程呢墓臭?跟協(xié)程簡(jiǎn)單來(lái)講就是最外層協(xié)程,還有一個(gè)特殊的規(guī)則就是妖谴,使用 SupervisorJob 創(chuàng)建的協(xié)程也視為根協(xié)程
窿锉。比如如下代碼:
CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
launch(CoroutineName("子協(xié)程1") + MyExceptionHandler() + SupervisorJob()) {
throw Exception("完蛋了,異常了")
}
}
輸出內(nèi)容為
CoroutineName(子協(xié)程1)中發(fā)生異常,完蛋了膝舅,異常了
說起處理異常嗡载,大家肯定想到 try / catch,為什么有了 try / catch仍稀,協(xié)程里還要有一個(gè) CoroutineExceptionHandler 呢洼滚?或者說 CoroutineExceptionHandler 到底起什么作用,什么時(shí)候用 CoroutineExceptionHandler 什么時(shí)候用 try / catch 呢技潘?官方文檔是這么描述 CoroutineExceptionHandler 的用于處理未捕獲的異常遥巴,是用于全局“全部捕獲”行為的最后一種機(jī)制千康。 你無(wú)法從CoroutineExceptionHandler的異常中恢復(fù)。 當(dāng)調(diào)用處理程序時(shí)铲掐,協(xié)程已經(jīng)完成拾弃。
,這段文字描述的很清楚了摆霉,這是全局(這個(gè)全局是指根協(xié)程作用域全局)的異常捕獲豪椿,是最后的一道防線,此時(shí)協(xié)程已經(jīng)結(jié)束斯入,你只能處理異常砂碉,而不能做其他的操作。舉個(gè)例子吧
CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
val test = 5 / 0
Log.i("zx", "即使異常了刻两,我也想繼續(xù)執(zhí)行協(xié)程代碼增蹭,比如:我要通知用戶,讓用戶刷新界面")
}
協(xié)程體中第一行 5/0 會(huì)拋出異常磅摹,會(huì)在 CoroutineExceptionHandler 中進(jìn)行處理滋迈,但是協(xié)程就會(huì)直接結(jié)束,后續(xù)的代碼不會(huì)再執(zhí)行户誓,如果想繼續(xù)執(zhí)行協(xié)程饼灿,比如彈出 Toast 通知用戶,這里就做不到了帝美。換成 try / catch 肯定就沒有問題了碍彭。
CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
try {
val test = 5 / 0
} catch (e: Exception) {
Log.i("zx", "我異常了")
}
Log.i("zx", "繼續(xù)執(zhí)行協(xié)程的其他代碼")
}
那既然如此,我直接把協(xié)程中所有代碼都放在 try / catch 里悼潭,不用 CoroutineExceptionHandler 不就行了庇忌?聽起來(lái)好像沒毛病,那我們就試試吧
inline fun AppCompatActivity.myLaunch(
crossinline block: suspend CoroutineScope.() -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
block()
} catch (e: Exception) {
Log.e("zx", "異常了舰褪," + e.message)
}
}
}
做了一個(gè)封裝皆疹,只要是調(diào)用封裝的 myLaunch 函數(shù),那所有的協(xié)程代碼都被 try / catch 包著占拍,這肯定沒問題了吧略就。比如我這樣調(diào)用
myLaunch {
val test = 5 / 0
}
程序沒崩,很好晃酒。換個(gè)代碼繼續(xù)調(diào)用
myLaunch {
launch {
val test = 5 / 0
}
}
APP 崩了表牢,不對(duì)呀,這里最外層明明已經(jīng)包了一層 try / catch掖疮,怎么捕獲不到呢初茶?想一下之前協(xié)程拋異常的規(guī)則:子協(xié)程會(huì)將異常一級(jí)一級(jí)向上拋,直到根協(xié)程
。這里用 launch 又新創(chuàng)建了一個(gè)子協(xié)程恼布,異常代碼運(yùn)行在子協(xié)程中螺戳,子協(xié)程直接把異常拋給了父協(xié)程,所以 try / catch 捕獲不到折汞。這里父協(xié)程又沒有指定異常處理器倔幼,所以就崩了。有人可能要抬杠了爽待,那我直接在子協(xié)程里 try / catch 不就不會(huì)崩了损同?確實(shí)不會(huì)崩了,這里你記住了加try / catch鸟款,那別的地方會(huì)不會(huì)忘了加呢膏燃。所以 CoroutineExceptionHandler 全作用域捕獲異常的優(yōu)勢(shì)就出來(lái)了。所以簡(jiǎn)單總結(jié)一下二者的區(qū)別和使用場(chǎng)景吧。
- CoroutineExceptionHandler 以協(xié)程為作用域全局捕獲未處理異常,可以捕獲子協(xié)程的異常缩滨,捕獲到異常時(shí)椿每,協(xié)程就已經(jīng)結(jié)束了球散。適用于做最后的異常處理以保證不崩潰,比如用來(lái)記錄日志等。
- try / catch 可以更加精細(xì)的捕獲異常,精確到一行代碼或者一個(gè)操作黍衙,無(wú)法捕獲子協(xié)程的異常,不會(huì)提前結(jié)束協(xié)程荠诬。適用于捕獲可以預(yù)知的異常琅翻。
以下是個(gè)人的心得,不一定正確柑贞,僅供參考望迎。
CoroutineExceptionHandler 適用于捕獲無(wú)法預(yù)知的異常。try / catch 適用于可以預(yù)知的異常凌外。 什么是可以預(yù)知的異常和不可預(yù)知的異常呢?舉個(gè)例子:你要往磁盤寫文件涛浙,可能會(huì)沒有權(quán)限康辑,也可能磁盤寫滿了,這些異常都是可以預(yù)知的轿亮,此時(shí)應(yīng)該用 try / catch疮薇。不可預(yù)知的異常就是指,代碼看起來(lái)沒毛病我注,但我不知道哪里會(huì)不會(huì)出錯(cuò)按咒,不知道 try / catch 該往哪里加,try / catch 有沒有少加但骨,這個(gè)時(shí)候就該交給 CoroutineExceptionHandler励七,畢竟 CoroutineExceptionHandler 是最后一道防線智袭。
CoroutineContext 總結(jié)
CoroutineContext 由 Job、CoroutineDispatcher掠抬、CoroutineName吼野、CoroutineExceptionHandler 組成。Job 可以控制協(xié)程的生命周期两波,也決定了子項(xiàng)異常時(shí)瞳步,父Job會(huì)不會(huì)取消。CoroutineDispatcher決定了協(xié)程運(yùn)行在哪個(gè)線程腰奋。CoroutineName給協(xié)程起名字单起,用于調(diào)試時(shí)區(qū)分。CoroutineExceptionHandler 用于全作用域捕獲并處理異常劣坊。子協(xié)程會(huì)自動(dòng)繼承父協(xié)程的CoroutineContext嘀倒,并可以覆蓋。CoroutineContext元素之間可以通過 + 運(yùn)算符組合讼稚,也可以通過對(duì)應(yīng)的key檢索出CoroutineContext中的元素括儒。
CoroutineStart 啟動(dòng)模式
上邊講了 launch 和 async 的第二個(gè)參數(shù)就是 CoroutineStart,也就是協(xié)程的啟動(dòng)模式锐想,共分為如下 4 種:
DEFAULT-默認(rèn)模式帮寻,立即調(diào)度協(xié)程;
LAZY-僅在需要時(shí)才懶惰地啟動(dòng)協(xié)程,使用
start()
啟動(dòng)赠摇;ATOMIC-原子地(以不可取消的方式)調(diào)度協(xié)程固逗,執(zhí)行到掛起點(diǎn)之后可以被取消;
-
UNDISPATCHED-同樣是原子地(以不可取消的方式)執(zhí)行協(xié)程到第一個(gè)掛起點(diǎn)藕帜。與ATOMIC的區(qū)別是:UNDISPATCHED不需要調(diào)度烫罩,直接執(zhí)行的,而ATOMIC是需要調(diào)度后再執(zhí)行的洽故;UNDISPATCHED是在父協(xié)程指定的線程中執(zhí)行贝攒,到達(dá)掛起點(diǎn)之后會(huì)切到自己上下文中指定的線程,ATOMIC是在自己的協(xié)程上下文中指定的線程執(zhí)行时甚。
需要注意的是調(diào)度(schedules)和執(zhí)行(executes)是不一樣的隘弊,調(diào)度之后并不一定是立即執(zhí)行。
分別舉例說明荒适。
LAZY 模式:
val job = lifecycleScope.launch(start = CoroutineStart.LAZY) {
Log.i("zx", "協(xié)程運(yùn)行了1")
}
上邊的代碼梨熙,并不會(huì)打印出內(nèi)容,需要手動(dòng)調(diào)用job.start()
,才能啟動(dòng)協(xié)程并打印出內(nèi)容刀诬。
ATOMIC 模式:
val job = lifecycleScope.launch(start = CoroutineStart.ATOMIC) {
Log.i("zx", "協(xié)程運(yùn)行了1")
delay(2000)
Log.i("zx", "協(xié)程運(yùn)行了2")
}
job.cancel()
由于使用的 ATOMIC 啟動(dòng)模式咽扇,執(zhí)行到掛起點(diǎn)之前(delay 是掛起函數(shù))是不能被取消的,所以無(wú)論如何都會(huì)打印出 "協(xié)程運(yùn)行了 1"。執(zhí)行到掛起點(diǎn)之后可以被取消质欲,所以不會(huì)打印出第二行树埠。
UNDISPATCHED 模式:
lifecycleScope.launch {
Log.i("zx", "父協(xié)程,當(dāng)前線程" + Thread.currentThread().name)
val job = launch(Dispatchers.IO, CoroutineStart.UNDISPATCHED) {
Log.i("zx", "子協(xié)程把敞,當(dāng)前線程" + Thread.currentThread().name)
delay(1000)
Log.i("zx", "子協(xié)程delay后弥奸,當(dāng)前線程" + Thread.currentThread().name)
}
}
上述代碼輸出
父協(xié)程,當(dāng)前線程main
子協(xié)程奋早,當(dāng)前線程main
子協(xié)程delay后盛霎,當(dāng)前線程DefaultDispatcher-worker-1
結(jié)果驗(yàn)證了,在到達(dá)第一個(gè)掛起點(diǎn)之前耽装,都是使用父協(xié)程所在線程去執(zhí)行協(xié)程愤炸,到達(dá)掛起點(diǎn)之后才會(huì)使用自己 coroutineContext 中設(shè)置的線程。類似于 ATOMIC 掉奄,在到達(dá)第一個(gè)掛起點(diǎn)之前同樣是不可取消的规个。
suspend 與 withContext
前邊反復(fù)提到掛起點(diǎn),那什么是掛起點(diǎn)呢姓建?什么又是掛起呢诞仓?掛起點(diǎn)實(shí)際上就是協(xié)程代碼執(zhí)行到 suspend 函數(shù)時(shí)的點(diǎn),此時(shí)協(xié)程會(huì)暫停速兔,suspend 函數(shù)之后的代碼不會(huì)再執(zhí)行墅拭,等到 suspend 函數(shù)執(zhí)行完之后,協(xié)程代碼會(huì)自動(dòng)繼續(xù)執(zhí)行涣狗。上邊用到的 delay 函數(shù)就是一個(gè)掛起函數(shù)谍婉,他會(huì)暫停(suspend)當(dāng)前協(xié)程代碼塊,先執(zhí)行 delay 函數(shù)镀钓,等 delay 執(zhí)行完后繼續(xù)執(zhí)行原有的代碼穗熬。先暫停,等代碼執(zhí)行完了在再自動(dòng)恢復(fù)(resume)執(zhí)行這個(gè)特性非常適合處理異步任務(wù)丁溅。例如如下代碼:
private suspend fun getBitmapByHttp(): Bitmap {
Log.i("zx", "當(dāng)前線程" + Thread.currentThread().name)
val url = URL("https://www.baidu.com/img/flexible/logo/pc/result.png");
val imageConnection = url.openConnection() as HttpURLConnection
imageConnection.requestMethod = "GET"
imageConnection.connect()
val inputStream: InputStream = imageConnection.inputStream
return BitmapFactory.decodeStream(inputStream)
}
lifecycleScope.launch {
val bitmap = getBitmapByHttp()//第一個(gè)行
viewBinding.imageView.setImageBitmap(bitmap)//第二行
}
先定義了一個(gè) suspend 函數(shù)唤蔗,這個(gè)函數(shù)從網(wǎng)絡(luò)加載圖片獲取到 bitmap。然后啟動(dòng)一個(gè) lifecycleScope 的協(xié)程窟赏,在里邊調(diào)用這個(gè) suspend 函數(shù)措译。應(yīng)該如我們所想,第一行是個(gè) suspend 函數(shù)饰序,是個(gè)掛起點(diǎn),會(huì)等到 getBitmapByHttp 執(zhí)行完再繼續(xù)執(zhí)行第二行 setImageBitmap规哪。然而運(yùn)行起來(lái)之后求豫,先是輸出 "當(dāng)前線程 main" 然后應(yīng)用崩了,拋出了 NetworkOnMainThreadException 異常,為什么這里的 suspend 函數(shù)會(huì)運(yùn)行在主線程呢蝠嘉?因?yàn)?suspend 并不知道具體要切到哪個(gè)線程最疆,所以依舊運(yùn)行在主線程。并且上述代碼蚤告,Android Studio 會(huì)提示 Redundant 'suspend' modifier
(多于的 suspend 修飾符)努酸。如何讓 suspend 函數(shù)切換到具體的線程呢?這就要用到 withContext 了杜恰。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
這是 withContext 的簽名获诈,可以看到 withContext 必須要傳入?yún)f(xié)程上下文以及一個(gè)協(xié)程代碼塊。協(xié)程上下文中包含了 Dispatchers心褐,它指定了 withContext 將要切到哪個(gè)線程中去執(zhí)行舔涎。withContext 也是一個(gè) suspend 掛起函數(shù),所以 withContext 執(zhí)行時(shí)逗爹,調(diào)用它的協(xié)程會(huì)先暫停亡嫌,等到它切到指定的線程并執(zhí)行完之后,會(huì)自動(dòng)再切回到調(diào)用它的協(xié)程掘而,并繼續(xù)執(zhí)行協(xié)程代碼挟冠。這其實(shí)就是掛起,自動(dòng)切走袍睡,執(zhí)行完了再自動(dòng)切回來(lái)繼續(xù)之前的操作
知染。同樣是之前的代碼,加上 withContext 之后就沒問題了女蜈。
private suspend fun getBitmapByHttp(): Bitmap = withContext(Dispatchers.IO) {
Log.i("zx", "當(dāng)前線程" + Thread.currentThread().name)
val url = URL("https://www.baidu.com/img/flexible/logo/pc/result.png");
val imageConnection = url.openConnection() as HttpURLConnection
imageConnection.requestMethod = "GET"
imageConnection.connect()
val inputStream: InputStream = imageConnection.inputStream
BitmapFactory.decodeStream(inputStream)
}
lifecycleScope.launch {
val bitmap = getBitmapByHttp()
viewBinding.imageView.setImageBitmap(bitmap)
}
既然 withContext 可以切走再切回來(lái)持舆,那調(diào)用時(shí)不要最外層的 lifecycleScope.launch {},不啟動(dòng)協(xié)程可以嗎伪窖。試了一下發(fā)現(xiàn) AS 提示錯(cuò)誤逸寓,編譯都過不了,提示"Suspend function 'getBitmapByHttp' should be called only from a coroutine or another suspend function"覆山,意思是掛起函數(shù)只能在另一個(gè)掛起函數(shù)或者協(xié)程里調(diào)用竹伸,那另一個(gè)掛起函數(shù)也只能在另另一個(gè)掛起函數(shù)或者協(xié)程里調(diào)用,如此套娃簇宽,最終就是掛起函數(shù)只能在一個(gè)協(xié)程里調(diào)用勋篓,這么限制是因?yàn)闀和!⑶凶呶焊睢⑶谢厝ゲ⒒謴?fù)執(zhí)行這些操作是由協(xié)程框架完成的譬嚣,如果不在協(xié)程里運(yùn)行,這些是沒法實(shí)現(xiàn)的钞它。
如果某個(gè)函數(shù)比較耗時(shí)拜银,我們就可以將其定義為掛起函數(shù)殊鞭,用 withContext 切換到非 UI 線程去執(zhí)行,這樣就不會(huì)阻塞 UI 線程尼桶。上邊的例子也展示了自定義一個(gè)掛起函數(shù)的過程操灿,那就是給函數(shù)加上 suspend 關(guān)鍵字,然后用 withContext 等系統(tǒng)自帶掛起函數(shù)將函數(shù)內(nèi)容包起來(lái)泵督。
試想一下趾盐,如果不用 suspend 和 withContext,那我們就需要自己寫開啟 Thread小腊,并自己用 Handler 去實(shí)現(xiàn)線程間通信救鲤。有了協(xié)程之后溢豆,這些都不需要我們考慮了,一下簡(jiǎn)單了很多麸折,更重要的是,這樣不會(huì)破壞代碼的邏輯結(jié)構(gòu),兩行代碼之間就像普通阻塞式代碼一樣,但是卻實(shí)現(xiàn)了異步非阻塞式的效果霉颠,這也就是非阻塞式的含義
小總結(jié):
- 掛起
就是一個(gè)切走再自動(dòng)切回來(lái)繼續(xù)執(zhí)行的線程調(diào)度操作,這個(gè)操作由協(xié)程提供不从,所以限制了suspend方法只能在協(xié)程里調(diào)用条舔。
- withContext
就是協(xié)程提供的一個(gè)掛起函數(shù)凄硼,起到的就是切到指定線程執(zhí)行代碼塊说墨,執(zhí)行完再切回來(lái)的作用突颊。
- suspend
僅僅只是一個(gè)限制治唤,限制了掛起函數(shù)只能在協(xié)程中調(diào)用棒动,并沒有實(shí)際的切線程
- 非阻塞式
寫法像普通阻塞式代碼一樣,卻實(shí)現(xiàn)了非阻塞式的效果宾添,沒有回調(diào)也沒有嵌套船惨,不破壞代碼邏輯結(jié)構(gòu)
- 自定義掛起函數(shù)
給函數(shù)加上suspend關(guān)鍵字并用withContext等系統(tǒng)自帶掛起函數(shù)將函數(shù)內(nèi)容包起來(lái)
簡(jiǎn)單使用
就像文章一開始那樣柜裸,就可以簡(jiǎn)單使用協(xié)程+Retrofit 發(fā)送異步網(wǎng)絡(luò)請(qǐng)求了,但是沒有異常處理粱锐,我們可以簡(jiǎn)單封裝一下加上異常處理以及 loading 顯示等疙挺。
全局協(xié)程異常處理
class GlobalCoroutineExceptionHandler(
val block: (context: CoroutineContext, exception: Throwable) -> Unit
) :
CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*>
get() = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, exception: Throwable) {
block(context, exception)
}
}
這里的 handleException 并沒有實(shí)際處理異常,實(shí)際處理異常的方法是外邊初始化 CoroutineExceptionHandler 時(shí)傳進(jìn)來(lái)的block怜浅。
Http 請(qǐng)求 Activity 基類
open class HttpActivity : AppCompatActivity() {
val httpInterface: HttpInterface = RetrofitFactory.httpInterface
private var progress: ProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
inline fun launchMain(
crossinline block: suspend CoroutineScope.() -> Unit
) {
val job = lifecycleScope.launch(GlobalCoroutineExceptionHandler(::handException)) {
showProgress()
block()
}
job.invokeOnCompletion {
hideProgress()
}
}
fun showProgress() {
if (progress == null) {
progress = ProgressDialog.show(
this, "", "加載中,請(qǐng)稍后...", false, true
)
}
}
fun hideProgress() {
if (progress != null && progress!!.isShowing) {
progress!!.dismiss()
progress = null
}
}
open fun handException(context: CoroutineContext, exception: Throwable) {
var msg = ""
if (exception is HttpException) {
msg = when (exception.code()) {
404 -> {
"$localClassName-異常了,請(qǐng)求404了铐然,請(qǐng)求的資源不存在"
}
500 -> {
"$localClassName-異常了,請(qǐng)求500了,內(nèi)部服務(wù)器錯(cuò)誤"
}
500 -> {
"$localClassName-異常了,請(qǐng)求401了恶座,身份認(rèn)證不通過"
}
else -> {
"$localClassName-http請(qǐng)求異常了,${exception.response()}"
}
}
} else {
msg = "$localClassName-異常了,${exception.stackTraceToString()}"
}
Log.e("zx", msg)
hideProgress()
Snackbar.make(
window.decorView,
msg,
Snackbar.LENGTH_LONG
)
.show()
}
}
定義了一個(gè) launchMain 函數(shù)搀暑,launchMain 中統(tǒng)一開啟協(xié)程,統(tǒng)一設(shè)置 CoroutineExceptionHandler跨琳,并會(huì)在請(qǐng)求時(shí)顯示環(huán)形進(jìn)度條自点,請(qǐng)求結(jié)束后隱藏進(jìn)度條。
定義了一個(gè) handException 函數(shù)脉让,這個(gè)函數(shù)是實(shí)際處理異常的函數(shù)桂敛,處理了一些常見的異常,并通過 Snackbar 顯示出來(lái)侠鳄。這里用的繼承埠啃,你也可以用擴(kuò)展函數(shù)去實(shí)現(xiàn)。
使用時(shí)讓 Activity 繼承 HttpActivity伟恶,然后就直接使用了,如果想自己處理異常碴开,實(shí)現(xiàn)handException函數(shù)就可以了。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launchMain {
val result = httpInterface.searchPhotos("china")
//更新UI
}
}
當(dāng)然這只是一種簡(jiǎn)單的用法博秫,沒有結(jié)合 ViewModel 和 LiveData潦牛,如果你不需要 MVVM,僅僅只需要在 Activity 中發(fā)請(qǐng)求挡育,或許可以考慮使用這種方式巴碗。