1. 引言
協(xié)程支持取消挡闰,也就是說,啟動(dòng)一個(gè)協(xié)程后而且在協(xié)程結(jié)束前已經(jīng)不希望協(xié)程再執(zhí)行代碼了掰盘,可以對(duì)協(xié)程進(jìn)行取消摄悯。
如果只知道協(xié)程的取消,而并不知道協(xié)程的取消需要代碼配合愧捕,將會(huì)導(dǎo)致不符合預(yù)期的執(zhí)行結(jié)果奢驯。
2. 協(xié)程的取消
其實(shí),在每次通過launch
啟動(dòng)協(xié)程次绘,該函數(shù)都會(huì)返回一個(gè)Job對(duì)象瘪阁。
val job = GlobalScope.launch(Dispatchers.IO) {
// ...
}
這個(gè)Job對(duì)象理解為協(xié)程的句柄撒遣,可以用來管理協(xié)程的生命周期。
協(xié)程的生命周期設(shè)計(jì)的內(nèi)容相當(dāng)豐富管跺,這里作為基礎(chǔ)篇部分不會(huì)鋪開所有的知識(shí)點(diǎn)(事實(shí)上义黎,如果鋪開這方面的知識(shí)點(diǎn),將會(huì)牽扯到協(xié)程非常多的設(shè)計(jì)概念和理解內(nèi)容豁跑,一下子吃太多廉涕,容易消化不良)。
本文主要介紹的內(nèi)容便是協(xié)程生命周期里的取消部分:在不再需要協(xié)程的時(shí)候艇拍,提前讓協(xié)程取消狐蜕。
取消的主要API,即使前面提及的Job對(duì)象的取消函數(shù):
job.cancel()
當(dāng)在某個(gè)頁面(比如Activity)中啟動(dòng)了一個(gè)協(xié)程卸夕,然后在頁面退出后层释,如協(xié)程仍未執(zhí)行完成,這時(shí)候最好將協(xié)程取消快集。
所以也很簡(jiǎn)單贡羔,在頁面內(nèi)所有啟動(dòng)協(xié)程的地方,將啟動(dòng)協(xié)程的Job對(duì)象保存个初,然后在頁面銷毀的回調(diào)中調(diào)用取消即可乖寒。
事實(shí)上,這樣是不夠的勃黍。取消不是萬能的,并不能在協(xié)程任意執(zhí)行點(diǎn)結(jié)束線程晕讲,而是需要協(xié)程執(zhí)行代碼中的API調(diào)用配合覆获。
3. 協(xié)程沒有達(dá)到取消效果的樣例
為了說明,這里寫出一段代碼瓢省,主要邏輯是啟動(dòng)協(xié)程弄息,協(xié)程內(nèi)的執(zhí)行主要邏輯為:5秒內(nèi)不斷循環(huán)打印log的操作,10秒后協(xié)程執(zhí)行完成勤婚。
如下:
val job = GlobalScope.launch(Dispatchers.IO) {摹量、
"Coroutine IO runs (from loopBtn)".let {
myLog(it)
}
var curMillis = System.currentTimeMillis()
val targetMilli = curMillis + FIVE_SECONDS
while (curMillis < targetMilli) {
val msg = "looping (from loopBtn)"
myLog(msg)
curMillis = System.currentTimeMillis()
}
"loop finished (from loopBtn)".let {
myLog(it)
}
}
然后通過一個(gè)List將job收集起來:
jobList.add(job)
然后在取消按鈕監(jiān)聽中將jobList中每個(gè)Job對(duì)象調(diào)用取消:
jobList.forEach {
it.cancel()
}
那么,請(qǐng)問馒胆,如果在啟動(dòng)協(xié)程后的5秒之內(nèi)缨称,點(diǎn)擊取消按鈕觸發(fā)協(xié)程的取消方法,協(xié)程最后一行的"loop finished (from loopBtn)"這一行l(wèi)og仍會(huì)不會(huì)輸出祝迂?
最終結(jié)果上睦尽,是會(huì)輸出的,即使在點(diǎn)擊了取消按鈕以后型雳,循環(huán)體中的log也仍然在不斷輸出当凡!
這便是山害,協(xié)程的取消并不是萬能,并不是說啟動(dòng)了協(xié)程后在協(xié)程結(jié)束前調(diào)用了取消函數(shù)沿量,協(xié)程自然就會(huì)中斷而停下浪慌!也就是說,在這段代碼當(dāng)中朴则,協(xié)程完全不會(huì)按照“理所當(dāng)然”地產(chǎn)生想要的取消效果权纤!
事實(shí)上,協(xié)程的取消是協(xié)作的佛掖,?段協(xié)程代碼必須協(xié)作才能被取消妖碉!
上面的協(xié)程代碼,即缺少了協(xié)作部分芥被!
4. 協(xié)程取消協(xié)作途徑——變量判斷
在協(xié)程取消協(xié)作當(dāng)中欧宜,一個(gè)不得不提的變量是isActive
通過這個(gè)變量,便可以知道協(xié)程在執(zhí)行的過程中是否已經(jīng)被取消拴魄。
事實(shí)上冗茸,這個(gè)變量在協(xié)程調(diào)用取消后,必然為false匹中,但是反過來并不會(huì)成立夏漱,也就是這個(gè)變量是false,并不能說明協(xié)程被取消顶捷。
所以挂绰,在第3節(jié)的代碼當(dāng)中,補(bǔ)充這個(gè)條件的判斷:
val job = GlobalScope.launch(Dispatchers.IO) {服赎、
"Coroutine IO runs (from loopBreakBtn)".let {
myLog(it)
}
var curMillis = System.currentTimeMillis()
val targetMilli = curMillis + FIVE_SECONDS
while (curMillis < targetMilli) {
if (!isActive) {
break
}
val msg = "looping (from loopBreakBtn)"
myLog(msg)
curMillis = System.currentTimeMillis()
}
"loop finished (from loopBreakBtn)".let {
myLog(it)
}
}
這樣就會(huì)發(fā)現(xiàn)葵蒂,當(dāng)這個(gè)job被取消以后,循環(huán)代碼中的log不再輸出重虑,也就是循環(huán)已經(jīng)跳出践付,但協(xié)程最后的一行l(wèi)og仍會(huì)輸出。
如果這個(gè)是理想效果缺厉,那么可以到底為止永高。
5. 協(xié)程取消協(xié)作途徑——函數(shù)檢測(cè)
事實(shí)上,更多的情況下的需求應(yīng)該是提针,在協(xié)程取消后命爬,不再執(zhí)行后續(xù)任何代碼,也就是說辐脖,僅僅跳出循環(huán)是不夠的遇骑,我們希望循環(huán)后面的所有代碼都不再執(zhí)行!
這種情況上揖曾,在協(xié)程取消協(xié)作上落萎,有更好的選擇亥啦,函數(shù)檢測(cè),具體的函數(shù)為:
ensureActive()
所以练链,進(jìn)一步得翔脱,將協(xié)程執(zhí)行代碼再次修改為如下執(zhí)行邏輯:
val job = GlobalScope.launch(Dispatchers.IO) {
"Coroutine IO runs (from loopEnsureBtn)".let {
myLog(it)
}
var curMillis = System.currentTimeMillis()
val targetMilli = curMillis + FIVE_SECONDS
while (curMillis < targetMilli) {
ensureActive()
val msg = "looping (from loopEnsureBtn)"
myLog(msg)
curMillis = System.currentTimeMillis()
}
"loop finished (from loopEnsureBtn)".let {
myLog(it)
stringBuilder.append(buildUIMsg(it))
}
}
這樣的話,在協(xié)程取消以后媒鼓,無論是循環(huán)體還是循環(huán)體后面的log届吁,都不會(huì)再輸出,達(dá)到了理想的協(xié)程取消效果绿鸣!
6. 樣例工程代碼
代碼樣例Demo疚沐,見Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代碼,如覺奇怪或啰嗦潮模,其實(shí)為CancelStepOneActivity.kt
中的代碼摘取主要部分說明亮蛔,在demo代碼當(dāng)中,為提升細(xì)節(jié)內(nèi)容擎厢,有更加多的封裝和輸出內(nèi)容究流。
本文不再列舉所有的log輸出內(nèi)容和情況進(jìn)行比較,而是直接給出結(jié)論动遭,具體的log輸出檢驗(yàn)可自行檢驗(yàn)芬探。
本文的頁面截圖如下:
7. 引申問題
本文所有的isActive
和ensureActive()
均是協(xié)程作用域的拓展屬性或方法,其實(shí)本質(zhì)也是協(xié)程上下文的同名變量或方法的進(jìn)一步封裝厘惦,而協(xié)程上下文偷仿,其實(shí)是協(xié)程所有的地方都可以拿到的變量,而協(xié)程作用域僅在個(gè)別協(xié)程API上可用宵蕉。
畫外音:忍自途病!不要展開国裳!這里是基礎(chǔ)篇不作鋪展形入!
最后全跨,必須強(qiáng)調(diào)缝左,本文的協(xié)程樣例代碼,是通過容器收集Job對(duì)象的方式來進(jìn)行的浓若,事實(shí)上渺杉,這是個(gè)非常不合理的做法!
一來挪钓,每次書寫啟動(dòng)協(xié)程方法的時(shí)候都要注意不能遺漏Job的收集是越,非常麻煩;
二來碌上,在協(xié)程執(zhí)行完成以后倚评,由于Job對(duì)象仍在容器當(dāng)中有引用浦徊,所以協(xié)程執(zhí)行完后并不能及時(shí)回收對(duì)象所占用內(nèi)存;
而這兩個(gè)問題天梧,通過協(xié)程作用域可以完美地自動(dòng)解決盔性!所以,接下來呢岗,才是認(rèn)識(shí)協(xié)程作用域的正確學(xué)習(xí)時(shí)機(jī)冕香!
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(一)協(xié)程啟動(dòng)
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(二)線程切換
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(三)初遇協(xié)程取消(本文)
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(四)協(xié)程作用域
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(五)再遇協(xié)程取消
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(六)初識(shí)掛起
一學(xué)就會(huì)的協(xié)程使用——基礎(chǔ)篇(七)初識(shí)結(jié)構(gòu)化