本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈android
協程經過替代回調(callback)來簡化異步代碼git
聽起來蠻抽象的,來看代碼github
fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
複製代碼
Android系統爲了保證界面的流暢和及時響應用戶的輸入事件,主線程須要保持每16ms一次的刷新(調用 onDraw()
函數),因此不能在主線程中作耗時的操做(好比 讀寫數據庫,讀寫文件,作網絡請求,解析較大的 Json 文件,處理較大的 list 數據)。數據庫
get()
經過接口獲取用戶數據,若是在主線程中調用fetchDocs()
函數就會阻塞(block)主線程,App 會卡頓甚至崩潰。數組
因此須要在子線程中調用get()
函數,這樣主線程就能夠刷新界面和處理用戶輸入,待get()
函數執行完畢後經過 callback 拿到結果。安全
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
複製代碼
callback 是個不錯的方式,可是 callback 被過分使用後代碼可讀性會變差(迷之縮進),並且 callback 不能使用 exception。爲了解決這樣的問題,歡迎協程(coroutine)閃亮登場
suspend fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
suspend fun get(url: String) =
withContext(Dispatchers.IO) {
...
}
複製代碼
明明是同步的寫法爲何不會阻塞主線程? 對,由於suspend
微信
被suspend
修飾的函數比普通函數多兩個操做(suspend 和 resume)網絡
get()
函數一樣也是一個suspend
函數。架構
suspend
修飾的函數並不意味着運行在子線程中併發
若是須要指定協程運行的線程,就須要指定Dispatchers ,經常使用的有三種:
經過withContext()
能夠指定Dispatchers,這裏的get()
函數裏的withContext
代碼塊中指定了協程運行在Dispatchers.IO中。
來看下這段代碼的具體執行流程
suspend
修飾的函數的時候,Kotlin須要追蹤正在運行的協程而不是正在執行的函數suspend
的標記,綠色上面的是協程,綠色下面的是一個正常的函數fetchDocs()
函數,在調用棧上加一個 entry,這裏也存儲着fetchDocs()
函數的局部變量suspend
函數的調用(這裏指的是 get()
函數調用),這時候Kotlin要去實現suspend
操做(將函數的狀態從堆棧複製到一個地方,以便之後保存,全部suspend
的協程都會被放在這裏)get()
函數,一樣新建一個entry,當調用到withContext()
(withContext函數被 suspend 修飾)的時候,一樣 執行suspend操做(過程和前面同樣)。此時主線程裏的全部協程都被 suspend,因此主線程能夠作其餘事情(執行 onDraw,響應用戶輸入)fetchDocs()
發成錯誤,會在這裏拋出異常val viewModelJob = Job() //用來取消協程
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) //初始化CoroutineScope 指定協程的運行所在線程傳入 Job 方便後面取消協程
uiScope.launch { //啓動一個協程
updateUI() //suspend函數運行在協程內或者suspend另一個函數內
}
複製代碼
suspend fun updateUI() {
delay(1000L) //delay是一個 suspend 函數
textView.text = "Hello, from coroutines!"
}
複製代碼
viewModelJob.cancel()//取消協程
複製代碼
CoroutineScope
,爲何須要? 一會解釋CoroutineScope
接受CoroutineContext
做爲參數,CoroutineContext
由一組協程的配置參數組成,能夠指定協程的名稱,協程運行所在線程,異常處理等等。能夠經過plus
操做符來組合這些參數。上面的代碼指定了協程運行在主線程中,而且提供了一個Job
,可用於取消協程
CoroutineName
(指定協程名稱)Job
(協程的生命週期,用於取消協程)CoroutineDispatcher
,能夠指定協程運行的線程Coroutine builders
來啓動協程,協程運行在Coroutine builders
的代碼塊裏面
Job
,可用來取消協程;有異常直接拋出CoroutineContext
改變協程運行的上下文Structured concurrency
)若是在
foo
裏協程啓動了bar
協程,那麼bar
協程必須在foo
協程以前完成
foo
裏協程啓動了bar
協程 ,可是bar
並無在 foo
完成以前執行完成,因此不是結構化併發
foo
裏協程啓動了
bar
協程 ,而且
bar
在
foo
完成以前執行完成,因此是結構化併發
結構化併發可以帶來什麼優點呢?下面一點點闡述。
儘管協程自己是輕量級的,可是協程作的工做通常比較重,好比讀寫文件或者網絡請求。使用代碼手動跟蹤大量的協程是至關困難的,這樣的代碼比較容易出錯,一旦對協程失去追蹤,那麼就會致使泄漏。這比內存泄漏更加嚴重,由於失去追蹤的協程在resume
的時候可能會消耗內存,CPU,磁盤,甚至會進行再也不必要的網絡請求。
如何避免泄漏呢?這其實就是CoroutineScope
的做用,經過launch
或者async
啓動一個協程須要指定CoroutineScope
,當要取消協程的時候只須要調用CoroutineScope.cancel()
,kotlin 會幫咱們自動取消在這個做用域裏面啓動的協程。
結構化併發能夠保證當一個做用域被取消,做用域裏面的全部協程會被取消
若是使用架構組件(Architecture Components),比較適合在ViewModel
中啓動協程,而且在onCleared
回調方法中取消協程
override fun onCleared() {
super.onCleared()
viewModelJob.cancel() //取消ViewModel中啓動的協程
}
複製代碼
本身寫CoroutineScope比較麻煩,架構組件提供了viewModelScope
這個擴展屬性,能夠替代前面的uiScope
。
看下viewModelScope
這個擴展屬性是如何實現的:
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(Job() + Dispatchers.Main))
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
複製代碼
一樣是初始化一個CoroutineScope,指定Dispatchers.Main和 Job
##ViewModel
@MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
onCleared();
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
複製代碼
clear()
中會自動取消做用域中的協程。有了viewModelScope
這個擴展屬性能夠少些不少模板代碼。
再看一個稍複雜的場景,同時發起兩個或者多個網絡請求。這就意味着要開啓更多的協程,隨處開啓協程可能致使潛在的泄漏問題,調用者可能不知道新開啓的協程,所以也無法追蹤他們。 這時候就須要coroutineScope
或者supervisorScope
(注意不是CoroutineScope
)。
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
launch { fetchDoc(2) }
}
}
複製代碼
這個示例中,同時發起兩個網絡請求。在suspend 函數裏面能夠經過coroutineScope
或 supervisorScope
安全地啓動協程。爲了不泄漏,咱們但願fetchTwoDocs
這樣的函數返回的時候,在函數內部啓動的協程都能執行完成。
結構化併發保證當suspend函數返回的時候,函數裏面的全部工做都已經完成
Kotlin能夠保證使用coroutineScope
不會從fetchTwoDocs
函數中發生泄漏,coroutineScope
會suspend
本身直到在它裏面啓動的全部協程執行完成。正是由於這樣,fetchTwoDocs
不會在coroutineScope
內部啓動的協程完成前返回。
若是有更多的協程呢?
suspend fun loadLots() {
coroutineScope {
repeat(1000) {
launch { fetchDoc(it) }
}
}
}
複製代碼
這裏在suspend函數中啓動了更多的協程,會泄露嗎?並不會。
因爲這裏的loadLots
是一個suspend
函數,因此loadLots
函數會在一個CoroutineScope
中被調用,coroutineScope
構造器會使用這個CoroutineScope
做爲父做用域生成一個新的CoroutineScope
。在coroutineScope
代碼塊內部,launch
函數會在這個新的CoroutineScope
中啓動新的協程,這個新的CoroutineScope
會追蹤這些新的協程,當全部的協程執行完畢,loadLots
函數纔會返回。
coroutineScope
和supervisorScope
會等到全部的子協程執行完畢。
使用coroutineScope
或者 supervisorScope
能夠安全地在suspend
函數裏面啓動新的協程,不會形成泄漏,由於老是會suspend
調用者直到全部的協程執行完畢。coroutineScope
會新建一個子做用域(child scope),因此若是父做用域被取消,它會把取消的信息往下傳遞給全部新的協程。
另外coroutineScope
和supervisorScope
的區別在於:coroutineScope
會在任意一個協程發生異常後取消全部的子協程的運行,而supervisorScope
並不會取消其餘的子協程。
前面有介紹過async
裏面若是發生異常是不會直接拋出的,直到 await
獲得調用,因此下面的代碼不會拋出異常。
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
複製代碼
可是coroutineScope
會等到協程執行完畢,因此發生異常後會拋出。下面的代碼會拋出異常。
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
複製代碼
結構化併發保證當協程出錯時,協程的調用者或者他的作用戶會獲得通知
因而可知 結構化併發能夠保證代碼更加安全,避免了協程的泄漏問題
suspend
函數返回,裏面的工做能保證完成,於是能夠追蹤正在執行的任務參考文檔:
Coroutines on Android (part I): Getting the background
Coroutines on Android (part II): Getting started