Koltin系列 - 協程從認識到安卓中的使用(五)

前言

學習了Kotlin一整個系列了,但是協程這塊遲遲沒有整理成一篇博文。誒,最近狀態有點不對 >_< || 。
但是無論如何,一定要加油!!最后一篇要劃上個完美點的句號,撒個漂亮點的花。

關于協程的一個點在這里跟大家先說一下,協程并非什么很深奧的東西,說白了也是在線程上面的產物,并非憑空產生的一個新的概念。官網講得可能有點高大上了,不過實際上你就當是它幫我們使用了線程池Handler進行一些自動切換線程的邏輯封裝進而形成了這樣子的一種API吧~~

協程的一些基礎使用

添加基本的依賴

implementation  'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

GlobalScope

官網定義:Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Another use of the global scope is operators running in Dispatchers.Unconfined, which don’t have any job associated with them.
Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.
大致意思就是:這個一般被用于頂級的協程,application生命周期級別的,不會過早的被取消。應用程序通常應該使用一個應用程序定義的CoroutineScope。使用異步或啟動的實例GlobalScope非常氣餒(不建議的)。

先來模擬一個場景,在一個ActivityA調用globalScopeLaunch,或者globalScopeLaunch進行耗時操作,類似IO操作或者網絡請求等。然后在它還沒有返回的時候銷毀ActivityA再跳轉到ActivityB

fun globalScopeLaunch(){
        GlobalScope.launch(Dispatchers.Main) {
            delay(5000)
            Toast.makeText(this@MainActivity,"等待五秒彈出~~~",Toast.LENGTH_LONG).show()
        }
    }

你會發現,它照樣會彈出這個Toast,但是這樣子其實并非我們想要的結果。有些事務我們應該隨著組件的生命周期結束而結束。否則一會造成資源的浪費或者內存泄露。(這里有個問題,如果你在生命周期結束的時候手動關閉的話,那就可以避免這種情況。但是這里就涉及到要你自己手動來控制了)

 private fun globalScopeLaunch1(){
        GlobalScope.launch(Dispatchers.Main) {
            launch (Dispatchers.Main){
                delay(1000)
                Toast.makeText(this@MainActivity,"等待一秒彈出",Toast.LENGTH_LONG).show()
            }
            Toast.makeText(this@MainActivity,"立馬彈出~~~",Toast.LENGTH_LONG).show()
            delay(5000)
            Toast.makeText(this@MainActivity,"等待五秒彈出~~~",Toast.LENGTH_LONG).show()
       }
    }

globalScopeLaunch1中,立馬彈出~~~ ->等待一秒彈出 ->"等待五秒彈出~~~。這里之所以會先彈出來立馬彈出~~~這個信息。因為協程中,又開了一個新的協程,新的協程阻塞一秒不關外邊協程的事情,外邊協程繼續執行。

private fun globalScopeLaunch(){
        job =  GlobalScope.launch(Dispatchers.Main) {
            launch (Dispatchers.Main){
                runBlocking {//加了runBlocking這個協程作用域
                delay(1000)
                Toast.makeText(this@MainActivity,"等待一秒彈出",Toast.LENGTH_LONG).show()
                }
            }
            Toast.makeText(this@MainActivity,"立馬彈出~~~",Toast.LENGTH_LONG).show()
            delay(5000)
            Toast.makeText(this@MainActivity,"等待五秒彈出~~~",Toast.LENGTH_LONG).show()
       }
    }

runBlocking會阻塞導致立馬彈出~~~這個Toast不會立刻顯示出來,而是等了1秒后,再彈出來。

上面的代碼可以簡化一下
在協程作用域中,可以使用withContext(Dispatchers.Main)替換launch (Dispatchers.Main)

job =  GlobalScope.launch(Dispatchers.Main) {
            withContext(Dispatchers.Main){} 
}

協程作用域

GlobalScope.launch(Dispatchers.Main)這里我是分發到主線程Main上面進行delay但是并不會造成ANR,可以簡單看一下launch怎么調用的

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

第三個參數 : block: suspend CoroutineScope.() 表示使用的協程作用域是CoroutineScope并不會造成阻塞。這里的阻塞是指協程作用域外的代碼阻塞,協程作用域內部還會被阻塞的。
CoroutineScopeGlobalScope的父類~

兩種協程作用域,以及結構化并發.png

協程的啟動方式launch與Async

private fun globalScopeAsync(){
  GlobalScope.launch(Dispatchers.Main){
  val deferred = async(IO) {
                Thread.sleep(5000)
                "等待五秒彈出~~~"
            }
    Toast.makeText(this@MainActivity,"立馬先彈出來~~",Toast.LENGTH_LONG).show()//這句是來驗證sync是不會阻塞改async協程外的代碼的
    val message =  deferred.await()
    Toast.makeText(this@MainActivity,message,Toast.LENGTH_LONG).show()
  }
}

async會異步跑該作用域外層的協程的邏輯,我們可以看到"立馬先彈出來~~"彈出框會先彈出來,再等過五秒在彈出 "等待五秒彈出~~~"再彈出來。在await這里會阻塞等待deferred返回回來再繼續接下來的操作。

協程的啟動方式.png

協程分發

image.png

協程的取消

獲取到對應協程的Job對象,調用cancel()

var job = GlobalScope.launch(Dispatchers.Main) { }
job.cancel()
//Deferred的對象父類是Job
var deferred =  GlobalScope.async {  }
deferred.cancel()

Android上使用協程的正確姿勢

MainScope

上面Global的官方定義中已經提示我們使用自定義的協程。
MainScopekotlin為我們自定義好的一個協程作用域。
代碼定義:

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

基本使用:

class MyAndroidActivity {
  private val scope = MainScope()
 //使用MainScope并賦予它名字
// val mainScope = MainScope() + CoroutineName(this.javaClass.name)
  private fun mainScopeLaunch(){
        scope.launch {}
  }
  override fun onDestroy() {
    super.onDestroy()
    scope.cancel()
  }
}

可以將這邏輯放到base類中

//無需定義協程名字的時候
open class BaseCoroutineScopeActivity : AppCompatActivity() , CoroutineScope by MainScope()

class MainActivity : BaseCoroutineScopeActivity(){

 private fun mainScopeLaunch(){
        launch {  }
    }
override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}
-----------------------------------------------------------------
//定義協程名字的時候
open class BaseCoroutineScopeActivity : AppCompatActivity() {
    val mainLaunch =  MainScope()+ CoroutineName(this.javaClass.simpleName)
}

class MainActivity : BaseCoroutineScopeActivity(){
 private fun mainScopeLaunch(){
        mainLaunch.launch {  }
    }
override fun onDestroy() {
        super.onDestroy()
        mainLaunch.cancel()
    }
}

ViewModelScope

使用該協程首先要導入包

api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'

代碼定義:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

這段代碼跟MainScope一樣,只是外面多了一層CloseableCoroutineScope的封裝,這個是為什么呢??
我們進去setTagIfAbsent看一下

   <T> T setTagIfAbsent(String key, T newValue) {
        T previous;
        synchronized (mBagOfTags) {
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
                mBagOfTags.put(key, newValue);
            }
        }
        T result = previous == null ? newValue : previous;
        if (mCleared) {
            closeWithRuntimeException(result);
        }
        return result;
    }

這里可以看出,當mCleared = true的時候它會自動幫我們關閉掉viewModelScope,也就是它幫我們處理生命周期的問題了 我們只管使用就可以。

使用:

fun requestAhuInfo() {
         viewModelScope.launch {         }
    }

LiveData && LifecycleScope 這兩個我自己并沒有使用。

推薦一下

秉心說TM的 - 如何正確的在 Android 上使用協程 ?
里面有說了這幾種kotlin為我們提供的協程

協程中的多種任務情況

  • 多個任務串行( launch+ withContext多個)
        viewModelScope.launch {
            var result1 = withContext(Dispatchers.IO) {
                Log.i(TAG, "result1-1")
                Log.i(TAG, "result1-2")
                Thread.sleep(4000)
                Log.i(TAG, "result1-3")
                "Hello"
            }
            var result2 = withContext(Dispatchers.IO) {
                Log.i(TAG, "result2-1")
                Log.i(TAG, "result2-2")
                Log.i(TAG, "result2-3")
                "world"
            }
            val result = result1 + result2
            Log.i(TAG, result)
        }
------------------------------------------------------------------------------------
2020-03-23 19:19:40.587 11021-11096/com.ldr.testcoroutines I/MainViewModel: result1-1
2020-03-23 19:19:40.587 11021-11096/com.ldr.testcoroutines I/MainViewModel: result1-2
2020-03-23 19:19:44.592 11021-11096/com.ldr.testcoroutines I/MainViewModel: result1-3
2020-03-23 19:19:44.602 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-1
2020-03-23 19:19:44.603 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-2
2020-03-23 19:19:44.603 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 19:19:44.603 11021-11021/com.ldr.testcoroutines I/MainViewModel: Helloworld
  • 多個任務并行( launch+ async多個)(launch + launch多個)
        viewModelScope.launch {
          val deferred =  async {
                Thread.sleep(4000)
                Log.i(TAG, "result1-1")
                Log.i(TAG, "result1-2")
                Log.i(TAG, "result1-3")
                "Hello"
            }
            val deferred1 = async {
                Log.i(TAG, "result2-1")
                Log.i(TAG, "result2-2")
                Log.i(TAG, "result2-3")
                "world"
            }
            var str = deferred.await() + deferred1.await()
            Log.i(TAG, str)
        }
------------------------------------------------------------------------------------
2020-03-23 18:52:28.016 9155-9155/com.ldr.testcoroutines I/MainViewModel: result1-1
2020-03-23 18:52:28.016 9155-9155/com.ldr.testcoroutines I/MainViewModel: result1-2
2020-03-23 18:52:28.016 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-1
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-2
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: Helloworld
  viewModelScope.launch {
            launch(Dispatchers.IO) {
                Log.i(TAG, "result1-1")
                Log.i(TAG, "result1-2")
                Thread.sleep(4000)
                Log.i(TAG, "result1-3")
            }

            launch(Dispatchers.IO) {
                Log.i(TAG, "result2-1")
                Log.i(TAG, "result2-2")
                Log.i(TAG, "result2-3")
            }
        }
---------------------------------------------------------------------------------
2020-03-23 19:21:29.781 11021-11128/com.ldr.testcoroutines I/MainViewModel: result1-1
2020-03-23 19:21:29.781 11021-11128/com.ldr.testcoroutines I/MainViewModel: result1-2
2020-03-23 19:21:29.782 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-1
2020-03-23 19:21:29.782 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-2
2020-03-23 19:21:29.782 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 19:21:33.782 11021-11128/com.ldr.testcoroutines I/MainViewModel: result1-3

協程的異常處理

  • CoroutineExceptionHandler
  val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }

    fun catchFun(): Unit {
        viewModelScope.launch(handler) {
            throw IOException()
        }
    }

 fun catch2Fun(): Unit {
        viewModelScope.launch(handler) {
            launch(Dispatchers.IO) {
                withContext(Dispatchers.Main){
                    throw IOException()
                }
            }
        }
    }
----------------------------------------------------------------------------------
2020-03-23 19:57:21.205 12038-12096/com.ldr.testcoroutines I/System.out: Caught java.io.IOException
2020-03-23 19:59:23.221 12038-12096/com.ldr.testcoroutines I/System.out: Caught java.io.IOException

經過上面的測試,可以知道CoroutineExceptionHandler這種方法可以將多層嵌套下的異常也捕獲到。

  • try { }catch(){}
//錯誤的寫法 協程外部是捕獲不到異常的
    fun catch1Fun(): Unit {
        try {
            viewModelScope.launch(Dispatchers.Main) {
                throw IOException()
            }
        }catch (e:Exception){
            Log.i(this.javaClass.name,e.cause?.message?:"拋出了異常")
        }
    }
//正確的寫法 好吧,,,,我覺得我在說廢話。。。
 fun catch1Fun(): Unit {
  viewModelScope.launch(Dispatchers.Main) {
     try {
           throw IOException()
        }catch (e:Exception){
            Log.i(this.javaClass.name,e.cause?.message?:"拋出了異常")
        }
    }
 }

附帶源碼地址: https://github.com/lovebluedan/TestCoroutines

總結

以上就是簡單的介紹了一下,協程的一些基本用法,關于里面很多原理性的東西,以后有機會再寫吧~~ 說實話,我并沒有用很深入,所以很多細節的東西還沒理解好。以往可以寫的深奧點,少點廢話少點代碼,文章寫得精煉點~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容