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

總結

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章