Kotlin協程實踐 - HTTP頁面內容異步下載示例

協程

相信你們都對協程這個詞頗有興趣,網上也有大量介紹協程的文章,可是大多數都是介紹概念及理論的,不多看到有使用實際案例的文章,筆者剛看到協程的概念時也是欣喜不已,以爲很是有用,很強大,能解決不少實際問題,可是總以爲不知道該如何下手去運用到實際項目中去,所以打算專門撰寫這篇協程實戰的文章,經過一個HTTP網頁URL頁面內容的下載實踐來說述解釋協程的原理與使用,但願能對你們學習使用並理解協程有所幫助。android

概念

協程 - 輕量級線程
雖然Kotlin中使用線程已經很方便了,但仍是推薦使用協程代替線程。
協程主要是讓原來要使用「異步+回調方式」寫出來的複雜代碼, 簡化成能夠用看似同步的方式寫出來(對線程的操做進一步抽象)。 這樣咱們就能夠按串行的思惟模型去組織本來分散在不一樣上下文中的代碼邏輯,而不須要去處理複雜的狀態同步問題,基本上也再也不須要接口處理代碼了。安全

先來看看以下代碼:網絡

fun startCoroutine(name: String) {
        println(" ### 1. Coroutine start in ${Thread.currentThread()}")
        val c1 = GlobalScope.launch(Dispatchers.Default) {
            println(" *** 2. ${name} launch start in ${Thread.currentThread()}")
            delay(1000)
            println(" *** 3. ${name} End of launch in ${Thread.currentThread()}")
        }

        println(" ### 4. Coroutine End. in ${Thread.currentThread()}")
    }


startCoroutine("CO1")
複製代碼

輸出結果:多線程

### 1. Coroutine start in Thread[main,5,main]
  ### 4. Coroutine End. in Thread[main,5,main]
    *** 2. CO1 launch start in Thread[DefaultDispatcher-worker-1,5,main]
    *** 3. CO1 End of launch in Thread[DefaultDispatcher-worker-3,5,main]
複製代碼

GlobalScope.launch(Dispatchers.Default) 用於啓動協程。 從輸出結果能夠看出,啓動協程以前,是在主線程中,可是協程啓動後,協程的代碼Block是在子線程中執行的。這不是重點,重點在於delay事後,協程的代碼必定是在子線程執行的,哪怕launch指定了Unconfined參數,協程一開始將在主線程中執行,可是delay依然不會阻塞主線程,但它的確能夠在指定的時間事後返回代碼塊繼續執行後面的代碼。這就是delay的強大之處,這個delay是不能夠在協程外部的代碼中調用的。app

協程調度器 功能描述
Dispatchers.Default 運行在 Dispatchers.Default 的線程池中
Dispatchers.Main 運行在主線程中
Dispatchers.IO 運行在 IO 線程中
Dispatchers.Unconfined 運行在當前線程中

PS:以前低版本的那套launch/await 全局函數已經廢棄,新版本必須使用GlobalScope.xxx。異步

協程的做用,就是讓開發者感受是在多線程中工做同樣,能夠異步處理耗時操做,但實際上可能並無真正使用線程,而就在同一線程中切換。協程的切換是由編譯器來完成的,於是開銷很小,並不依賴系統資源,你能夠開100000個協程,而沒法啓動100000個線程。async

delay跟線程的sleep很類似,都是延時一段時間,可是不一樣點在於,delay不會阻塞當前線程,而是掛起協程自己,從而將線程資源釋放出來,供其它協程使用。函數

咱們所必需要了解的是,在協程中,當你的耗時任務作完以後,你的代碼極可能不在剛纔的線程當中,此時必需要注意代碼的線程安全問題,例如訪問UI,你可使用runOnUiThread { }。學習

在startCoroutine的結尾處,可使用c1.join()來等待協程結束,一旦使用join,編譯器便提醒必須添加suspend關鍵字,該函數也必須在協程中調用。測試

再來看看修改後的代碼:

suspend fun startCoroutine(name: String) {
    println(" ### 1. Coroutine start in ${Thread.currentThread()}")
    val c1 = GlobalScope.launch(Dispatchers.Default) {
        println(" *** 2. ${name} launch start in ${Thread.currentThread()}")
        delay(3000)
        println(" *** 3. ${name} End of launch in ${Thread.currentThread()}")
    }
    c1.join()
    println(" ### 4. Coroutine End. in ${Thread.currentThread()}")
}
複製代碼

該方法由於添加了suspend關鍵字,所以只能在協程中調用:

GlobalScope.launch(Dispatchers.Main) {
        startCoroutine("CO1")
    }
複製代碼

輸出結果以下:

### 1. Coroutine start in Thread[main,5,main]
     *** 2. CO1 launch start in Thread[DefaultDispatcher-worker-2,5,main]
     *** 3. CO1 End of launch in Thread[DefaultDispatcher-worker-3,5,main]
   ### 4. Coroutine End. in Thread[main,5,main]
複製代碼

能夠看到,代碼中的日誌順序,是按一、二、三、4的順序輸出的了,join函數會等待協程結束。因爲我指定了startCoroutine在Dispatchers.Main父協程中運行,所以當join等待子協程完成以後,又回到了主線程執行,這種方式來更新UI的話,都再也不須要使用runOnUiThread了,很適合用於作動畫。

協程實戰

咱們經過一個網絡URL加載Web數據的實例,來展現協程對於異步處理的強大之處。
首先,須要在build.gradle中添加:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
複製代碼

在AndroidManifest.xml中添加:

<uses-permission android:name="android.permission.INTERNET" />
複製代碼

新建一個UrlDownload類:

class UrlDownload {
    // kotlin沒有static方法,而是要使用伴生對象來替代
    companion object {
        suspend fun asyncDownload(url: String): String? {
            return GlobalScope.async(Dispatchers.Default) {
                download(url)
            }.await()
        }

        fun download(url: String): String {
            var urlConn : HttpURLConnection? = null
            var strBuffer = StringBuffer()
            var inputStream: InputStream? = null
            var buffer: BufferedReader? = null
            var inputReader: InputStreamReader? = null

            try {
                urlConn = URL(url).openConnection() as HttpURLConnection
                inputStream = urlConn.getInputStream()
                inputReader = InputStreamReader(inputStream)
                buffer = BufferedReader(inputReader)
                do {
                    var line = buffer.readLine()
                    strBuffer.append(line)
                } while (line != null)

            } catch (e: Exception){
                e.printStackTrace()
            } finally {
                inputReader?.close()
                buffer?.close()
                inputStream?.close()
                urlConn?.disconnect()
            }

            return strBuffer.toString()
        }
    }
}

fun startDownload() {
    var url = "https://m.weibo.cn/"
    GlobalScope.launch(Dispatchers.Default) {
        var content = UrlDownload.asyncDownload(url) // 這是一個異步執行的耗時的操做
        println(content)
    }
}

複製代碼

執行以上程序,在主線程調用startDownload()函數,能夠看到控制檯打印出了網頁內容。請注意整個程序沒有定義任何回調接口,但結果的確是在業務層打印出來的,閱讀代碼就好像是同步執行的同樣,你也能夠看的出,以上代碼並不會阻塞主線程。

  • download(url: String)是一個同步方法,實現聯網返回網頁數據的功能,該方法會阻塞當前線程,不能在主線程調用。
  • asyncDownload方法添加了suspend關鍵字,說明該函數將被掛起並異步執行,等到異步執行完畢纔會返回結果。
  • suspend關鍵字聲明的函數,是一個掛起函數,只能在協程裏面調用。
  • 編譯器將每個掛起點的先後做爲獨立的代碼片斷,這些代碼片斷在須要的時候纔會執行,不會阻塞當前線程,內部使用狀態機來保證協程狀態的恢復以及代碼片斷的順序執行。
  • 執行了掛起方法以後,沒法肯定是在哪一個線程恢復執行,除非指定了Dispatchers.Main調度器。

若是須要一層一層的往上傳遞,那麼將startDownload作個簡單改造便可:

suspend fun startDownload(url: String): String? {
    return GlobalScope.async(Dispatchers.Default) {
        UrlDownload.asyncDownload(url)
    }.await()
}

fun appStartDownload() {
    var url = "https://m.weibo.cn/"
    GlobalScope.launch(Dispatchers.Default) {
        var content = startDownload(url)
        println(content)
    }
}
複製代碼

GlobalScope.launch 啓動一個協程,並返回這個協程對象,咱們能夠調用 join()來等待協程結束,join沒有返回值。
await() 則有返回值,能夠返回數據,要使用await(),必須使用GlobalScope.async來啓動協程。再來看看上述啓動代碼的學習修改版本:

suspend fun startDownload(url: String): String? {
        println("### 1. startDownload start in ${Thread.currentThread()}")
        var r =  GlobalScope.async(Dispatchers.Default) {
            println(" ### 2. startDownload in ${Thread.currentThread()}")
            UrlDownload.asyncDownload(url)
            println(" ### 3. startDownload in ${Thread.currentThread()}")
        }.await()

        println("### 4. startDownload End. in ${Thread.currentThread()}")
        return "### startDownload TEST ###"
    }
複製代碼

輸出結果以下:

### 1. startDownload start in Thread[DefaultDispatcher-worker-1,5,main]
     ### 2. startDownload in Thread[DefaultDispatcher-worker-2,5,main]
     ### 3. startDownload in Thread[DefaultDispatcher-worker-3,5,main]
   ### 4. startDownload End. in Thread[DefaultDispatcher-worker-3,5,main]
複製代碼

從日誌能夠看出,雖然日誌順序也是嚴格按照代碼中一、二、三、4的順序執行的,可是4號日誌跟1號日誌已經不在同一個線程,而是跟3號日誌在同一個線程。這就是異步等待await的結果,因此該方法必須使用suspend關鍵字,告訴編譯器這個是協程函數,必須在協程中調用。否則隨意切換客戶代碼的線程,確定要出亂子的。這就是協程的關鍵,也是協程的強大之處,可是越是強大的東西,使用時必定要知道它的特色,雖然使用起來很簡單。

剛纔的代碼,有一個費解的地方:

suspend fun startDownload(url: String): String? {
    return GlobalScope.async(Dispatchers.Default) {
        UrlDownload.asyncDownload(url) // 其實await是將這行代碼的返回結果做爲返回值了
    }.await()
}
複製代碼

那麼細心的同窗可能會問,若是我在這裏寫了兩行代碼呢?既然是實戰學習,固然不能放過這個問題,繼續編寫學習測試代碼:

suspend fun startDownload(url: String): String? {
    return GlobalScope.async(Dispatchers.Default) {
        UrlDownload.asyncDownload(url) 
        UrlDownload.asyncDownload("https://www.xxx.com/")
        "### 返回值 ###"
    }.await()
}
複製代碼

測試發現,await會將最後一個表達式的值做爲返回值,而前面的多個asyncDownload都會執行,並且是順序執行,緣由是asyncDownload內部自己也使用了協程await()來等待,咱們把那個協程叫子協程,啓動子協程的協程叫父協程。那麼若是咱們但願兩個下載任務可以同時並行進行呢,固然有辦法,那就是再啓動一個新的父協程去執行UrlDownload.asyncDownload便可,要知道kotlin中是能夠啓動100000個協程的,上線只受內存限制。

至此,相信讀者對於協程的概念、使用都能很好的理解了,測試代碼就再也不貼出來了,有興趣的同窗能夠自行編寫代碼來驗證,以加深理解。

Kotlin快速入門 - 安卓開發新趨勢,Java轉Kotlin開發,花一天時間就夠了 以前寫的一篇文章,可是不知怎麼在掘金髮布不了,因而附上一個簡書連接。

相關文章
相關標籤/搜索