給Android開發者的Kotlin協程入門講解--從線程到Kotlin協程

前言

  擁有協程的編程語言已經有不少了,它們各自對協程的概念都有不一樣的定義,可是,爲了更好的理解Kotlin的協程,請不要摻雜任何其它語言的協程概念,讓咱們從線程提及,相信看完幾個示例事後,沒有接觸過協程的Android開發者也能掌握到協程的基本使用方法。java

咱們是怎樣使用線程的

咱們用一段代碼來演示一下咱們是怎樣使用線程的:android

class ThreadActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_list)
        recyclerView.addItemDecoration(SpaceItemDecoration(24))
        //建立一個線程並啓動
        Thread {
            try {
                //發起HTTP請求
                val weChatAuthorsJson = getWeChatAuthorsJson()
                //反序列化
                val list = weChatAuthorListDeserialization(weChatAuthorsJson)
                //切換到UiThread
                runOnUiThread {
                    recyclerView.adapter = WeChatAuthorAdapter(list)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }.start()
    }

    private fun getWeChatAuthorsJson(): String {
        val request = Request.Builder()
            .url("https://wanandroid.com/wxarticle/chapters/json")
            .build()
        val call = OkHttpClient().newCall(request)
        val response = call.execute()
        return response.body!!.string()
    }

    private fun weChatAuthorListDeserialization(json: String): List<WeChatAuthor> {
        val typeToken = object : TypeToken<ApiResponse<List<WeChatAuthor>>>() {}
        val apiResponse = Gson()
            .fromJson<ApiResponse<List<WeChatAuthor>>>(json, typeToken.type)
        return apiResponse.data
    }

}
複製代碼

  咱們建立了一個線程,先請求網絡獲取了一段JSON,而後使用Gson把JSON反序列化爲了一個List,而後在子線程中經過runOnUiThread方法切換到了主線程中,爲列表裝配了適配器後,RecyclerView中呈現了數據。git

  runOnUiThread方法的使用雖然方便,可是卻帶來了一個問題。咱們能夠在runOnUiThread以前加上Thread.sleep(5000)這行代碼,讓子線程暫停5秒,再在runOnUiThread裏面加一行輸出日誌的代碼:github

Thread {
    try {
        //發起HTTP請求
        val weChatAuthorsJson = getWeChatAuthorsJson()
        //反序列化
        val list = weChatAuthorListDeserialization(weChatAuthorsJson)
        //讓線程暫停5秒
        log("準備暫停線程")
        Thread.sleep(5000)
        //切換到UiThread
        runOnUiThread {
            log("準備顯示列表")
            recyclerView.adapter = WeChatAuthorAdapter(list)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}.start()
複製代碼

  而後運行程序,咱們在列表顯示以前按返回鍵,再觀察Logcat:編程

2019-10-31 19:35:47.947 21592-21693/com.numeron.coroutine D/ThreadActivity: Thread:Thread-3	準備暫停線程
2019-10-31 19:35:52.965 21592-21592/com.numeron.coroutine D/ThreadActivity: Thread:main	準備顯示列表
複製代碼

  能夠看到:即便是咱們已經按下了返回鍵退出了Activity,可是runOnUiThread裏面的代碼依然執行了,看上去好像沒有問題,可是實際上,這會帶來空指針以及內存泄漏的風險。 若是咱們使用Kotlin協程實現以上相同的功能的話,由於Kotlin協程是能夠被取消的,因此咱們也能夠避免這個狀況的出現。   json

Kotlin協程的基本使用

首先,咱們須要添加依賴:api

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1'
複製代碼

而後,新建一個Activity,編寫如下代碼:bash

class CoroutineActivity : AppCompatActivity() {

    private lateinit var job: Job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_list)
        recyclerView.addItemDecoration(SpaceItemDecoration(24))
        //在IO調度器上啓動一個協程
        job = GlobalScope.launch(Dispatchers.IO) {
            try {
                //發起HTTP請求獲取json
                val weChatAuthorsJson = getWeChatAuthorsJson()
                //反序列化
                val list = weChatAuthorListDeserialization(weChatAuthorsJson)
                //讓協程暫停5秒
                log("準備暫停協程")
                delay(5000)
                //切換到UiThread顯示列表
                withContext(Dispatchers.Main) {
                    log("準備顯示列表")
                    recyclerView.adapter = WeChatAuthorAdapter(list)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        //在協程結束時,打印一行日誌,並輸出錯誤信息,若是有錯誤的話
        job.invokeOnCompletion {
            log("協程執行結束", it)
        }
    }

    override fun onDestroy() {
        job.cancel("Activity onDestroy.")
        super.onDestroy()
    }

    private fun log(msg: String, e: Throwable? = null) {
        Log.d("CoroutineActivity", "Thread:${Thread.currentThread().name}\t" + msg, e)
    }

    private suspend fun getWeChatAuthorsJson(): String {
        return suspendCancellableCoroutine {
            val request = Request.Builder()
                .url("https://wanandroid.com/wxarticle/chapters/json")
                .build()
            val call = OkHttpClient().newCall(request)
            //當協程取消時,取消請求
            it.invokeOnCancellation {
                call.cancel()
            }
            val response = call.execute()
            val responseBody = response.body
            if (responseBody == null) {
                it.resumeWithException(NullPointerException())
            } else {
                it.resume(responseBody.string())
            }
        }
    }

    private suspend fun weChatAuthorListDeserialization(json: String): List<WeChatAuthor> {
        return suspendCoroutine {
            val typeToken = object : TypeToken<ApiResponse<List<WeChatAuthor>>>() {}
            try {
                val apiResponse =
                    Gson().fromJson<ApiResponse<List<WeChatAuthor>>>(json, typeToken.type)
                it.resume(apiResponse.data)
            } catch (e: Exception) {
                it.resumeWithException(e)
            }
        }
    }

}
複製代碼

咱們經過GlobalScope.launch方法啓動了一個協程,指定它在IO調度器上運行,一樣的,咱們先是發送請求獲取JSON,而後反序列化爲List,再經過withContext切換到主線程中,爲RecyclerView裝配適配器,GlobalScope.launch方法會返回一個Job對象,咱們將它保存起來,並重寫Activity的onDestroy方法,在onDestroy方法中添加一行:網絡

job.cancel()
複製代碼

用於在Activity銷燬時,取消協程的運行。咱們把程序跑進來,並在顯示列表以前,按下返回鍵,觀察Logcat:編程語言

2019-10-31 20:31:25.882 32011-32055/com.numeron.coroutine D/CoroutineActivity: Thread:DefaultDispatcher-worker-1	準備暫停協程
...
2019-10-31 20:31:27.160 32011-32055/com.numeron.coroutine D/CoroutineActivity: Thread:DefaultDispatcher-worker-1	協程執行結束
    java.util.concurrent.CancellationException: Activity onDestroy.
        ...
複製代碼

咱們在Logcat中找不到「準備顯示列表」的日誌記錄,而且出現了「協程執行結束」的日誌記錄,這就說明了:在咱們調用了job.cancel()以後,協程沒有再繼續運行下去了。
可是仔細看過getWeChatAuthorsJson()方法和weChatAuthorListDeserialization()方法後,發現了幾個不太明白的地方:
  1.方法上多了一個suspend關鍵字,它是幹嗎的?
  2.suspendCoroutine和suspendCancellableCoroutine又是幹嗎用的?
關於這兩個問題,涉及到了suspend的特性:

  • suspend方法只能在協程和其它的suspend方法中調用
  • suspend方法最終會在協程中被調用,也就是說,suspend的消費者是協程。
  • 最開始的suspend方法是由suspendCoroutine方法和suspendCancellableCoroutine方法建立的,也就是說,它們是suspend的生產者。

什麼狀況下應該用suspend關鍵字來修飾方法呢?

  • 想調用其它suspend方法時,應該添加suspend關鍵字
  • 方法中要執行耗時的操做時,應該使用suspendCoroutine方法或suspendCancellableCoroutine方法來將一個普通方法轉換爲suspend方法
    suspendCoroutine方法和suspendCancellableCoroutine方法的使用方法請參考weChatAuthorListDeserialization()和getWeChatAuthorsJson()方法的實現。

回到代碼中來,雖然咱們解決了內存泄漏的問題,可是還有另外一個問題:咱們把job保存爲全局變量,若是要同時執行多個協程,那不是要建立多個job變量?這太不優雅了!是的,因此官方已經爲咱們提供了一個推薦的寫法。

優雅的使用Kotlin協程

咱們讓Activity實現CoroutineScope接口,而後經過Kotlin的by關鍵字,把它代理給MainScope():

class CoroutineActivity : AppCompatActivity(), CoroutineScope by MainScope()
複製代碼

接下來,咱們把全局成員job刪除,之後也再也不使用GlobalScope來啓用Kotlin協程:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_list)
    recyclerView.addItemDecoration(SpaceItemDecoration(24))
    //在IO調度器上啓動一個協程
    launch(Dispatchers.IO) {
        try {
            //發起HTTP請求獲取json
            val weChatAuthorsJson = getWeChatAuthorsJson()
            //反序列化
            val list = weChatAuthorListDeserialization(weChatAuthorsJson)
            //讓協程暫停5秒,把delay換成Thread.sleep也是同樣的效果
            log("準備暫停協程")
            delay(5000)
            //切換到UiThread顯示列表
            withContext(Dispatchers.Main) {
                log("準備顯示列表")
                recyclerView.adapter = WeChatAuthorAdapter(list)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }.invokeOnCompletion {  //在協程結束時,打印一行日誌,並輸出錯誤信息,若是有錯誤的話
        log("協程執行結束", it)
    }
}
複製代碼

最後,修改onDestroy中的代碼:

override fun onDestroy() {
    cancel("Activity onDestroy.")
    super.onDestroy()
}
複製代碼

以上,就是按照官方推薦的寫法修改後的實現了,無論在Activity中經過launch方法啓動了多少個Kotlin協程,只要onDestroy方法運行了,全部正在運行的Kotlin協程都會被取消掉。

結語

總的來講,在Android開發的過程當中,對於線程的需求基本上只有兩個:

  • 切換線程。
  • 及時終止線程的運行。
    而Kotlin協程能夠知足咱們的需求,非但如此,Kotlin協程遠不是本文這樣三言兩語就能講明白的,本文全是我的看法,若有不當,還請不吝賜教! 最後,我的使用Kotlin協程、JetPack開發的示例工程,求小星星:github.com/xiazunyang/…
相關文章
相關標籤/搜索