前言
拥有协程的编程语言已经有很多了,它们各自对协程的概念都有不同的定义,但是,为了更好的理解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/…