给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/…
相关文章
相关标签/搜索