看上去連續的一段代碼,執行起來卻走走停停,不一樣的子代碼段還可能執行在不一樣的線程上。協程就是用這種方式來實現異步
。android
最開始,在沒有協程和各類異步工具時,只能這樣實現異步:web
// 構建主線程 Handler
val mainHandler = Handler(Looper.getMainLooper()) // 啓動新線程 val handlerThread = HandlerThread("user") handlerThread.start() // 構建新線程 Handler val handler = Handler(handlerThread.looper) // 把"拉取用戶信息"經過 Handler 發送到新線程執行 handler.post(object : Runnable { override fun run() { val user = fetchUser() //執行在新線程 // 把用戶信息經過 Handler 發送到主線程執行 mainHandler.post(object : Runnable { override fun run() { tvName.text = user.name //執行在主線程 } }) } }) Log.v("test", "after post") // 會馬上打印(主線程不被阻塞) fun fetchUser(): User { Thread.sleep(1000) //模擬網絡請求 return User("taylor", 20, 0) } 複製代碼
這段代碼從網絡獲取用戶數據並顯示在控件上。數據庫
代碼的不一樣部分會執行在不一樣線程上:拉取用戶信息的耗時操做會在handlerThread
線程中執行,而界面顯示邏輯在主線程。編程
這兩個線程間步調不一樣(異步),即互不等待對方執行完畢再執行本身的後續代碼(不阻塞)。它們經過進程間互發消息實現了異步。緩存
這樣寫的缺點是在同一層次中暴露太多細節!構建並啓動線程的細節、線程切換的細節、線程通訊的細節、網絡請求的細節。這些本該被隱藏的細節通通在業務層被鋪開。網絡
若改用RxJava
就能夠屏蔽這些細節:多線程
userApi.fetchUser()
.observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe( { user -> tvName.text = user.name }, { error -> Log.e("error","no user") } ) 複製代碼
RxJava 幫咱們切換到 IO 線程作網路請求,再切換回主線程展現界面。線程間通訊方式也從發消息變爲回調。代碼可讀性瞬間提高。架構
若需求改爲「獲取用戶信息後再根據用戶 ID 獲取其消費流水」,就得使用flatMap()
將兩個請求串聯起來,此時不可避免地出現嵌套回調,代碼可讀性降低。併發
若用協程,就能夠像寫同步代碼同樣寫異步代碼:異步
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.text) // 啓動頂層協程 GlobalScope.launch { // 拉取用戶信息(掛起點) val user = fetchUser() // 拉取用戶帳單(掛起點) val bills = fetchBill(user) // 展現用戶帳單(UI操做) showBill(bills) } Log.v("test", "after launch") // 馬上打印(主線程不被阻塞) } // 掛起方法 suspend fun fetchUser(): User { delay(1000) // 模擬網絡請求 return User("taylor", 20, 0) } // 掛起方法 suspend fun fetchBill(user: User): List<Bill> { delay(2000) // 模擬網絡請求 return mutableListOf(Bill("Tmall", 10), Bill("JD", 20)) } } 複製代碼
GlobalScope.launch()
啓動了一個協程,主線程不會被阻塞(「after launch」會當即打印)。其中GlobalScope
是CoroutineScope
的一個實現。
CoroutineScope
稱爲 協程領域,它是協程中最頂層的概念,全部的協程都直接或間接的依附於它 ,它用於描述協程的歸屬,定義以下:
// 協程領域
public interface CoroutineScope { // 協程上下文 public val coroutineContext: CoroutineContext } // 協程領域的靜態實現:頂層領域 public object GlobalScope : CoroutineScope { // 空上下文 override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext } 複製代碼
協程領域 持有 CoroutineContext
CoroutineContext
稱爲 協程上下文 ,它是「和協程執行相關的一系列元素的集合」,其中最重要的兩個是CoroutineDispatcher
(描述協程代碼分發到哪一個線程執行)和Job
(表明着協程自己)。
協程領域 有一個靜態實現GlobalScope
,它用於建立頂層協程,即其生命週期同 App 一致。
協程的啓動方法被定義成CoroutineScope
的擴展方法:
/** * 啓動一個新協程,它的執行不會阻塞當前線程。默認狀況下,協程會被當即執行。 * * @param context 在原有協程上下文基礎上附加的上下文 * @param start 協程啓動選項 * @param block 協程體,它會在協程上下文指定的線程中執行 **/ public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext,// 默認爲空上下文 start: CoroutineStart = CoroutineStart.DEFAULT, // 默認啓動選項 block: suspend CoroutineScope.() -> Unit ): Job { ... } 複製代碼
啓動協程時,必須提供參數block
(協程體),即在協程中執行的代碼段。
Demo 在協程體中前後調用了兩個帶suspend
的方法。
suspend
方法稱爲 掛起方法。掛起的對象是其所在協程,即協程體的執行被暫停。被暫停的執行點稱爲 掛起點,執行掛起點以後的代碼稱爲 恢復。
Demo 中有兩個掛起點:在用戶信息不返回以前,拉取帳單就不會被執行,在拉取帳單不返回以前,就不會把數據填充到列表中。
執行下 Demo,看看效果:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. 複製代碼
崩潰緣由是「展現帳單邏輯被執行在非UI線程」。GlobalScope.launch()
將協程體調度到新線程執行,執行完耗時操做後,UI 展現時還須要調度回主線程:
GlobalScope.launch {
val user = fetchUser() val bills = fetchBill(user) withContext(Dispatchers.Main) { showBill(bills) } } 複製代碼
withContext()
是一個頂層掛起方法:
public suspend fun <T> withContext( context: CoroutineContext,// 指定 block 被調度到哪一個線程執行 block: suspend CoroutineScope.() -> // 被調度執行的代碼段 ): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> ... } 複製代碼
它用於在協程中切換上下文(切換協程體執行的線程)。withContext()
會掛起當前協程(它是一個掛起點),直到block
執行完,協程纔會在本身原先的線程上恢復執行後續代碼。
上面的例子是兩個串行請求,若是換成「等待兩個並行請求的結果」,能夠這樣寫:
GlobalScope.launch {
val a = async { fetchA() } val b = async { fetchB() } a.await() // 掛起點 b.await() // 掛起點 Log.v("test","result=${a+b}")// 當兩個網絡請求都返回後纔會打印 } suspend fun fetchA(): String { ...// 網絡請求 } suspend fun fetchB(): String { ...// 網絡請求 } 複製代碼
在頂層協程中又調用async()
啓動了2個子協程:
// 啓動協程,並返回協程執行結果
public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T> { ... } 複製代碼
aync()
也是CoroutineScope
的擴展方法,和launch()
惟一的不一樣是它引入了泛型,用於描述協程體執行的結果,並將其包裝成一個Deferred
做爲返回值:
public interface Deferred<out T> : Job {
// 掛起方法: 等待值的計算,但不會阻塞當前線程,計算完成後恢復當前協程執行 public suspend fun await(): T } 複製代碼
調用async()
啓動子協程不會掛起外層協程,而是當即返回一個Deferred
對象,直到調用Deferred.await()
,協程的執行纔會被掛起。當協程在多個Deferred
對象上被掛起時,只有當它們都恢復後,協程才繼續執行。這樣就實現了「等待多個並行的異步結果」。
若是多個並行的異步操做沒有返回值,如何等待它們都執行完畢?
GlobalScope.launch {
// 掛起外層協程 coroutineScope { // 和外層協程體執行在同一個線程中 launch { updateCache() } launch { insertDb() } } Log.v("test", "after coroutineScope()") // 被coroutineScope阻塞,等其執行完畢纔打印 } suspend fun updateCache() { ...// 更新內存緩存 } suspend fun insertDb() { ...// 插入數據庫 } 複製代碼
coroutineScope()
建立了一個協程並阻塞當前協程,在其中調用launch()
建立了2個子協程,只有當2個子協程都執行完畢後纔會打印 log。
coroutineScope()
聲明以下:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
... } 複製代碼
coroutineScope()
有以下特色:
返回計算結果 阻塞當前協程 執行在和父協程相同的線程中 它等待全部子協程執行完畢
coroutineScope() 是 withContext() 的一種狀況,當給withContext()
傳入當前協程上下文時,它和 coroutineScope()
如出一轍。它也會返回計算結果,也會阻塞當前線程,也會等待全部子協程執行完畢。
換句話說,coroutineScope()
是 不進行線程調度的 withContext()
。
雖然上面這些代碼都是正確的,但它們不應出如今真實項目中。
由於它們都使用GlobalScope.launch()
來啓動協程。這樣作會讓管理協程變得困難:
GlobalScope.launch()
構建的協程是獨立的,它不隸屬於任何CoroutineScope
。並且是靜態的,因此生命週期和 APP 一致。
一旦被建立則要等到 APP 關閉時纔會釋放線程資源。若在短生命週期的業務界面使用,需純手動管理生命週期,不能享受structured-concurrency
。
structured-concurrency 是一種併發編程範式,它是管理多線程併發執行生命週期的一種方式,它要求「執行單元」的孵化要有結構性,即新建的「執行單元」必須依附於一個更大的「執行單元」。這樣就便於管理(同步)全部執行單元的生命週期。
Kotlin 協程實現了該範式,具體表現爲:
新建協程必須隸屬於一個 CoroutineScope
,新協程的Job
也就成爲CoroutineScope
的子Job
。父 Job
被結束時,全部子Job
立馬被結束(即便還未執行完)。父 Job
會等待全部子協程都結束了才結束本身。子 Job
拋出異常時,會通知父Job
,父Job
將其餘全部子Job
都結束。
先看一個手動管理協程生命週期的例子:若是一個 Activity 全部的協程都經過GlobalScope.launch()
啓動,那在 Activity 退出時,該如何取消這些協程?
辦法仍是有的,只要在每次啓動協程時保存其Job
的引用,而後在Activity.onDestroy()
時遍歷全部Job
並逐個取消:
class TestActivity : AppCompatActivity(){
// 持有該界面中全部啓動協程的引用 private var jobs = mutableListOf<Job>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 啓動頂層協程並保存其引用 GlobalScope.launch { ... }.also { jobs.add(it) } } override fun onMessageReceive(msg: Message) { // 啓動頂層協程並保存其引用 GlobalScope.launch { ... }.also { jobs.add(it) } } override fun onDestroy() { super.onDestroy() // 將全部協程都取消以釋放資源 jobs.forEach { it.cancel() } } } 複製代碼
每個GlobalScope.launch()
都是獨立的,且它不隸屬於任何一個CoroutineScope
。爲了管理它們就必須持有每一個啓動協程的引用,並逐個手動釋放資源。
若使用structured-concurrency
範式就可讓管理變簡單:
class TestActivity : AppCompatActivity(), CoroutineScope by MainScope() {{
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) launch { ... } } override fun onMessageReceive(msg: Message) { launch { ... } } override fun onDestroy() { super.onDestroy() cancel() } } 複製代碼
Activity 實現了CoroutineScope
接口並將其委託給MainScope()
:
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
複製代碼
MainScope()
是一個頂層方法,它新建了一個ContextScope
實例,併爲其指定上下文,其中一個是Dispatchers.Main
,它是系統預約義的主線程調度器,這意味着,MainScope
中啓動的協程體都會被調度到主線程執行。
launch()
和cancel()
都是 CoroutineScope
的擴展方法,而 Activity 實現了該接口並委託給MainScope
。因此 Demo 中經過launch()
啓動的協程都隸屬於MainScope
,onDestroy
中調用的cancel()
取消了MainScope
的Job
,它的全部子Job
也一同被取消。
Activity 被建立的時CoroutineScope
同時被實例化,在 Activity 被銷燬時,全部的協程也被銷燬,實現了協程和生命週期對象綁定。 不再用擔憂後臺任務完成後更新界面時,因 Activity 已銷燬報空指針了。
協程能夠和任何具備生命週期的對象綁定,好比 View,只有當 View 依附於界面時其對應的協程任務纔有意義,因此當它與界面解綁時應該取消協程:
// 爲 Job 擴展方法
fun Job.autoDispose(view: View) { // 判斷傳入 View 是否依附於界面 val isAttached = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null // 若是 View 已脫離界面則直接取消對應協程 if (!isAttached) { cancel() } // 構建 View 和界面綁定關係監聽器 val listener = object : View.OnAttachStateChangeListener { // 當 View 和界面解綁時,取消協程 override fun onViewDetachedFromWindow(v: View?) { cancel() v?.removeOnAttachStateChangeListener(this) } override fun onViewAttachedToWindow(v: View?) = Unit } // 爲 View 設置監聽器 view.addOnAttachStateChangeListener(listener) // 當協程執行完畢時,移除監聽器 invokeOnCompletion { view.removeOnAttachStateChangeListener(listener) } } 複製代碼
而後就能夠像這樣使用:
launch {
// 加載圖片 }.autoDispose(imageView) 複製代碼
GlobalScope
沒法和任何生命週期對象綁定(除 App 生命週期),除了這個缺點外,還有一個:
coroutineScope { GlobalScope.launch { queryA() } GlobalScope.launch { queryB() } } 複製代碼
當queryB()
拋出異常時,queryA()
不會被取消。由於它們是經過GlobalScope.launch()
啓動的,它們是獨立的,不隸屬於外層coroutineScope
。
但若換成下面這種方式,queryA()
就會被取消:
coroutineScope { launch { queryA() } launch { queryB() } } 複製代碼
由於這裏的launch()
都是外層coroutineScope
對象上的調用,因此它們都隸屬於該對象。當子協程拋出異常時,父協程會受到通知並取消掉全部其餘子協程。
上一節的代碼雖然是正確的,但依然不應出如今真實項目中。由於 Activity 屬於View層
,只應該包含和 UI 相關的代碼,啓動協程執行異步操做這樣的細節不應在這層暴露。(架構相關的詳細討論能夠點擊我是怎麼把業務代碼越寫越複雜的 | MVP - MVVM - Clean Architecture)
真實項目中,協程更有可能在ViewModel
層出現。只要引入 ViewModel Kotlin 版本的包就能夠輕鬆地在ViewModel
訪問到CoroutineScope
:
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha03"
複製代碼
class MainViewModel : ViewModel() {
fun fetchBean() { // 系統爲 ViewModel 預約義的 CoroutineScope viewModelScope.launch { ... } } } 複製代碼
viewModelScope
被定義成ViewModel
的擴展屬性,這種擴展手法頗爲巧妙,限於篇幅緣由,準備單獨寫一篇詳細分析。
這篇僅粗略地介紹了協程相關的概念、協程的使用方式,及注意事項。依然留了不少疑惑,好比:
CoroutineScope
這個角色?啓動協程爲啥要定義成
CoroutineScope
的擴展函數?
CoroutineContext
的內部結構是怎麼樣的?爲啥要這樣設計?
下一篇將更加深刻閱讀源碼,解答這些疑問。
本文使用 mdnice 排版