本文主要是學習筆記,有版權問題還請告知刪文
鳴謝:guolin@第一行代碼(第三版)
你是否也在最近的代碼中看見了 AsyncTask 被一條橫杠划掉了
這表明——他要被Google放棄了
Google說讓我們換成協程,也就是Coroutine,我們來看看怎么無縫切換
1. 添加依賴
首先,他並不是在標准庫里,所以我們要添加依賴
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
}
2. 使用協程
2.1 GlobalScope.launch
最簡單的方法就是調用GlobalScope.launch
這里每創建一個協程都是頂層協程!對!沒錯!協程是分層級
GlobalScope.launch {
//do something
}
這個函數的特點就是:自己悶頭執行,主線程結束,他也結束(雖然有可能沒執行完)
2.2 runBlocking
runBlocking從字面意思就可以理解,運行並阻塞,意思是運行協程,並在攜程未結束時阻塞當前線程
runBlocking{
//do something
}
這個函數的特點就是:執行時阻塞當前線程
此方法一般用在測試環境,實際使用中容易有性能問題
2.3 多協程
多協程只要我們這么寫就可以
runBlocking{
launch{
//do something#1
}
launch{
//do something#2
}
launch{
//do something#3
}
}
這樣我們就創建了3個子協程
注意,我們這里的launch和GlobalScope.launch有一些區別,GlobalScope創建的是頂層協程
另外,我們的多協程還是運行在一個線程里面的,也就是3個協程運行在一個線程里面
到這里,相信你們都有了一種奇怪的感覺,一個線程里實現並發???
我要說的是:沒錯!
因此它不需要操作系統的參與,像是子協程有一百萬個,它也不會產生OOM
我測試了1000000個協程總共消耗了5722ms,也就是5秒
高並發效率非常高
2.4 suspend關鍵字(能在協程里調用的函數)
別擔心,這跟Java廢棄的那個強制掛起線程的函數沒什么關系,只是恰巧長得一樣而已@Java suspend()
當你的代碼過多導致可讀性很差的時候,就需要將部分代碼寫成一個函數,這時候就需要用到suspend,這樣我們就可以在協程作用域里面調用該函數
用guolin大神的話說,就是“suspend可以將我們的函數聲明成掛起函數”
我理解的是,掛起函數可以在其內部調用其他的掛起函數,比如:delay()
suspend fun 函數名(參數列表){
//do something
}
但是suspend並不會讓我們的函數就像在協程作用域里寫的一樣,比如:無法調用launch創建子協程
這時我們就需要調用corountineScope函數
suspend fun 函數名(參數列表) = corountineScope{
launch{
//do something
}
}
corountineScope 是會在其作用域里的程序執行完成前阻塞當前協程的,但它不會影響其他協程,也不會影響線程
而且它只能在 協程作用域 或 掛起函數 下調用
runBlocking {
launch {
println("即將被阻塞")
coroutineScope{
for (i in 1..3){
println("阻塞中")
delay(10)
}
}
println("阻塞結束")
}
launch {
println("我沒被阻塞")
}
}
這個運行結果就會是這樣的
即將被阻塞
阻塞中
我沒被阻塞
阻塞中
阻塞中
阻塞結束
3. 取消協程(關掉它!!!)
launch函數會返回一個Job類型的對象
Job類里面有一個cancel函數,調用它就可以取消協程
val job = Job()
val scope = CoroutineScope(job)
scope.launch{
//do something
}
job.cancel()
這就是一般情況下項目里協程的寫法
這里還有一個要注意的,第二行的CoroutineScope(job)並不是CoroutineScope的構造函數,而是一個單獨的函數
至於為啥Kotlin要把這個函數名首字母大寫,我也不知道...
4. 獲得協程運行結果
4.1 async
假設我們在需要進行一個耗時操作,我們將這個任務放到協程里,可我們不知道什么時候能獲得這個結果,而launch最終也只是返回一個Job對象,這樣的機制不利於我們處理結果,所以為了獲得我們需要的結果,async函數就必不可少了,async會新建一個子協程並返回一個Deferred對象,Deferred里有一個await函數,他就會給我們想要的結果
還有一點相信你已經想到了,await會阻塞當前的協程,直到獲得我們想要的結果
寫法如下
runBlocking {
val result = async {
//do something
}.await()
}
其中常量result就是我們需要的結果
這里需要注意的是,如果你有多個結果需要獲取,一定要明白其中的邏輯關系然后優化好代碼
比如:我們需要用A和B生成E,C、D生成F,E、F生成result
代碼就應該這么寫
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
val E = async {
//A和B生成E
}
val F = async {
//C和D生成F
}
val result = async {
//E.await() 和 F.await() 生成
}
}
4.2 withContext
寫法如下
runBlocking {
val result = withContext(Dispatchers.Default) {
//do something
}
}
還是引用guolin大神的話:withContext可以理解為async的簡化版寫法。
調用 withContext 會立刻執行代碼塊里的代碼,同時將當前的協程阻塞。代碼塊執行完后,會將最后一行的結果當做返回值返回。唯一不同的是他有個參數。
簡而言之,就是沒有 await 函數,沒法靈活控制協程阻塞,除此之外多了個參數,其他和async區別不大
guolin大神還說了:withContext是強制要求輸入參數的。而剛才提到的除了coroutineScope,其他所有的協程作用域構建器也都可以指定參數,只不過不強制
那么這個參數是干嘛的呢???
答案是用來選擇並發線程策略的,這種參數主要有三種可選:Dispatchers.Default、Dispatchers.IO、Dispatchers.Main
是的,你沒聽錯,線程策略,是線程,也就是說,他會創建線程來幫助實現並發,要知道網絡請求是只能在線程里實現並發的,協程是不行的。
Dispatchers.Default | 默認的低並發線程策略 |
---|---|
Dispatchers.IO | 高並發線程策略 |
Dispatchers.Main | 不開啟線程 |
5. 回調簡寫(suspendCoroutine)
我們回調的通常寫法是:
HttpUtil.sendHttpRequest(address : String , object : HttpCallbackListener {
override fun onFinish() {
//do something
}
override fun onError() {
//do something
}
})
類似這樣,代碼就很多,動個手指頭都嫌累
Kotlin就給了我們一個解決策略
用suspendCoroutine函數!
suspendCoroutine必須在協程域中或掛起函數里調用
它接受一個lambda表達式參數,主要作用是將當前協程立刻掛起,然后在一個普通線程(guolin大神的話,我理解的是非主線程就行)里執行lambda表達式中的代碼。
lambda表達式又傳入一個Continuation參數,調用它的resume() 或 resumeWithException() 就可以讓協程恢復執行。
代碼示例:
suspend fun request(address: String): String {
return suspendCoroutine { contination ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
override fun onFinish(Response: String) {
contination.resume(Response)
}
override fun onError(e: Exception) {
contination.resumeWithException(e)
}
})
}
}
然后你就想罵我,這TM不是更多了嗎???
你調用試試...
suspend fun getBaiduResult(){
try{
val result = request ( "https://www.baidu.com" )
//對result進行處理
}catch{
//異常處理
}
}
爽不爽!?