给Android开发者的Kotlin协程入门讲解--从线程到Kotlin协程

1,940 阅读6分钟

前言

  拥有协程的编程语言已经有很多了,它们各自对协程的概念都有不同的定义,但是,为了更好的理解Kotlin的协程,请不要掺杂任何其它语言的协程概念,让我们从线程说起,相信看完几个示例过后,没有接触过协程的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中呈现了数据。

  runOnUiThread方法的使用虽然方便,但是却带来了一个问题。我们可以在runOnUiThread之前加上Thread.sleep(5000)这行代码,让子线程暂停5秒,再在runOnUiThread里面加一行输出日志的代码:

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协程是可以被取消的,所以我们也可以避免这个情况的出现。   

Kotlin协程的基本使用

首先,我们需要添加依赖:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1'

然后,新建一个Activity,编写以下代码:

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/…