Kotlin 協程由淺入深

在這裏插入圖片描述

任何UI框架都有自己的主線程來進行渲染界面和觀察觸摸事件,這個線程絕對是你應該關注的最重要的線程,用戶永遠不知道你是否使用了AsyncTask或者Coroutine來編寫你的代碼,但是用戶卻可以感受到你的應用的響應速度,因此要如何組織應用所要執行的任務是你要最應該瞭解的事情之一

  1. HelloWorld

    如果你有一個網絡請求,耗時很久,那麼它必須不能在主線程執行。如果你在Android應用裏這樣做,將會導致你的應用崩潰。最常見的一種解決方案是在後臺線程執行網絡請求,然後再回調裏返回結果供UI更新(比如Retrofit最簡單的使用),還有大家所熟悉的Rxjava,可以在後臺線程訂閱網絡請求,在主線程得到請求結果(而且還是鏈式調用,非常nice)。但是現在你又多了一種方案,使用協程

    對於普通的函數,你可以嵌套調用,當然程序也會是順序執行,下面舉例說明我們在main函數調用funA時會發生什麼

    fun main() {
        funA()
    }
    fun funA(){
        println("start...")
        funB()
        funC()
        println("....end")
    }
    fun funB(){
        println("Hello")
    }
    fun funC(){
        println("World!")
    }
    

    說了這麼多了,但是上面的問題看起來很可笑,因爲它太簡單了,但請耐心看下去。很現實程序會順序執行,每當需要調用另外一個函數的時候,程序都會先去執行那個函數的代碼,等到執行完畢返回調用的地方繼續執行後面的代碼

    我們讓上述的情況變得再複雜一點,我們可以想象funB和funC是執行網絡請求或者從本地數據庫讀取數據,那它們就需要消耗一些時間。我們通過延遲函數執行來模擬這種耗時操作

    fun main() {
        //Kotlin提供的計算耗時時間的一個函數
        val timeMillis = measureTimeMillis {
            funA()
        }
        println("cost time $timeMillis")
        println("Exiting main...")
    }
    fun funA(){
        println("start....")
        funB()
        funC()
        println("....end")
    }
    fun funB(){
        Thread.sleep(1000)//線程睡眠阻塞1s
        println("Hello")
    }
    fun funC(){
        Thread.sleep(1000)
        println("World!")
    

    你應該知道上述的例子執行的結果,如下

    start....
    Hello
    World!
    ....end
    cost time 2001
    Exiting main...
    

    正如我們所想,當程序開始執行會立即執行第一行代碼,1s之後輸出Hello,再過1s後輸出World!,然後是輸出執行時間,最後main執行結束。程序耗時了2s多,我們可以把它的執行時間縮短到1s嗎

    於是我們想到了併發,我們可以給funB和funC分別開啓一個線程來達到目的。利用線程來處理耗時操作,Java和Andoid也提供了一些Api供我們時使用,我們在這裏姑且就用最簡單的線程來處理提升程序的執行效率

    fun main() {
        val timeMillis = measureTimeMillis {
            funA()
            Thread.sleep(1300)//阻塞主線程
        }
        println("cost time $timeMillis")
        println("Exiting main...")
    }
    fun funA(){
        println("start....")
        thread { funB() }
        thread { funC() }
        println("....end")
    }
    fun funB(){
        Thread.sleep(1000)//線程睡眠阻塞1s
        println("Hello")
    }
    fun funC(){
        Thread.sleep(1000)
        println("World!")
    

    輸出結果如下

    start....
    ....end
    Hello
    World!
    cost time 1308
    Exiting main...
    

    我們利用線程讓程序利用之前一半的時間完成了任務,但這只是理想結果,實際我們在實際中利用線程寫出一個沒有併發錯誤的代碼還需要其它很多知識,另一方面線程也很耗內存,一不小心就會內存溢出OutOfMemoryError,這個時候解決問題就得從語法和資源兩個角度去解決問題了,如果你不差錢,可以無休止的加內存(但這是不現實的),如果=一種完美的解決方案,我們不需要修改任何東西,就可以編寫出高效的代碼又可以避免出現內存溢出的情況就好了,哈哈哈。

    Kotlin 可能是比較接近這種理想情況的一種解決方案,你可以做出儘可能少的重構將你之前的同步代碼變成異步的

  2. 協程

    fun main() {
        GlobalScope.launch {
            funA()
            println("current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
        }
        Thread.sleep(2300)
        println("Exiting main...current thread is ${Thread.currentThread().name}")//current thread is main
    }
    suspend fun funA() {
        println("start....current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
        funB()
        funC()
        println("....end==current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
    }
    suspend fun funB() {
        delay(1000)//線程睡眠阻塞1s
        println("current thread is ${Thread.currentThread().name}--Hello")//current thread is DefaultDispatcher-worker-1
    }
    suspend fun funC() {
        delay(1000)
        println("World!--current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
    }
    

    從代碼上我們對funA B C 函數前都加了suspend,使用delay代替了Threed.sleep;從執行結果看起來和我們第一個沒有時使用併發工具一樣,也耗時了2s多,我們利用協程是否也可以縮短執行時間到1s呢

  3. 並行

    fun main() {
        GlobalScope.launch {
            val millsCoroutine = measureTimeMillis { funA() }
            println("cost time $millsCoroutine")
        }
        Thread.sleep(2300)
        println("Exiting main")
    }
    suspend fun funA(){
        println("start...")
        println("${funB()} ${funC()}")
        println("...end")
    }
    suspend fun funB():String{
        delay(1000)
        return "Hello"
    }
    suspend fun funC():String{
        delay(1000)
        return "World!"
    }
    

    同樣花費了2s時間,我們可以用Async和wait縮短運行時間,他們是協程的一個構建器,可以並行執行代碼,每一段代碼外面套上async{},在async裏的代碼塊執行結束,它就會返回一個Deffered的數據結構,從而得到你期望的結果

    fun main() {
        GlobalScope.launch {
            val millsCoroutine = measureTimeMillis { funA() }
            println("cost time $millsCoroutine")
        }
        Thread.sleep(2300)
        println("Exiting main")
    }
    suspend fun funA(){
        println("start...")
        val helloDeffered = GlobalScope.async { funB() }
        val worldDeffered = GlobalScope.async { funC() }
        println("${helloDeffered.await()} ${worldDeffered.await()}")
        println("...end")
    }
    

    執行結果,現在協程以併發模式運行

    start...
    Hello World!
    ...end
    cost time 1018
    Exiting main
    
  4. 結構化併發(Structured Concurrency)

    無論你是使用常規的回調還是更優雅的Rxjava,你都必須在Activity或者Fragment生命週期結束後取消後臺線程的操作,纔不會可能造成的內存泄露。如果你熟悉autodispose ,它是一個開源庫用來在組件被銷燬時自動取消Rxjava訂閱,因此你不必手動處理

    協程有Structured Concurrency的概念,對我們來說這是一個新的,陌生的名詞,但是它解決了上一段提出的問題(不需要時取消後臺任務),Structured Concurrency只是制定了一些標準,這些標準規定了併發程序有清晰的入口點和出口點

  5. Coroutine Scope 協程作用域

    結構化併發標準之一就是不能在協程作用域之外開啓協程。我們上面的例子使用GlobalScope這個協程作用域,它是一個特殊的作用域,作用於整個應用。

    android的架構組件裏都提供了默認的協程作用域,比如liveData,ViewModel,LifeCycle。因此大多時候你都不需要自己實現CoroutineScope,如果你想這樣做,你只需要讓你的class實現CoroutineScope接口並定義上下文

    class WelcomingScreen : Activity, CoroutineScope by CoroutineScope(Dispatchers.Default) {}
    
  6. Coroutine Context

    每一個協程作用域CoroutineScope都需要一個上下文,協程上下文是Element實例的索引集,其中一個Element實例就是我們需要執行協程的工作線程,稱之爲Dispatchers調度器

    如果你使用默認調度器定義了父級協程作用域,那所有子協程默認都會繼承這個上下文(調度器),除非你在launch函數或者withContext函數傳入新的上下文參數,這也是你切線程的方式

    回到上面的例子WelcomingScreen,我們需要在IO線程調用async,然後在主線程得到結果,我們可以這樣做,先修改funA

    suspend fun funA(): String {
        println("start....")    
        val helloDeffered = async(Dispatchers.IO) { funB() }
        val worldDeffered = async(Dispatchers.IO) { funC() }    
        println("......end")   
        return helloDeffered.await() + " " + worldDeffered.await()}
    

    再修改activity,再IO線程執行funA,再UI線程打印返回結果

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)    setContentView(R.layout.activity_main)    
        launch {
            println("Launch Thread: ${Thread.currentThread().name}")        
            val string = funA()        
            println("Logging Thread: ${Thread.currentThread().name}")        withContext(Dispatchers.Main) {
                Toast.makeText(
                    this@WelcomeScreen, string,Toast.LENGTH_LONG).show()
            }
        }
    }
    

    得到了我們想要的結果,再IO線程執行復雜操作,再主線程處理執行結果

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