众所周知在 android 中当执行程序的耗时超过 5 秒时就会引发 ANR 而导致程序崩溃。由于 UI 的更新操作是在 UI 主线程进行的,理想状态下每秒展示 60 帧时人眼感受不到卡顿,1000ms/60 帧,即每帧绘制时间不应超过 16.67ms。如果某项操作的耗时超过这一数值就会导致 UI 卡顿。因此在实际的开发中我通常把耗时操作放在一个新的线程中(比如从网络获取数据,从 SD 卡读取图片等操作),但是呢在 android 中 UI 的更新只能在 UI 主线程中进行更新,因此当我们在非 UI 线程中执行某些操作的时候想要更新 UI 就需要与 UI 主线程进行通信。在 android 中 google 为我们提供了 AsyncTask 和 Handler 等工具来便捷的实现线程间的通信。有许多的第三方库也为我们实现了这一功能,比如现在非常流行的 RxJava 库。在本篇文章中呢我想给大家分享的是使用 Kotlin 的 Coroutine(协程)来实现耗时操作的异步加载,现在有 RxJava 这么屌的库我们为什么还要了解这个呢?Kotlin 如今已是 android 的官方开发语言了解他里边的异步相关的操作是很有必要的。本文只讲解 Coroutine 的基本使用方法,并不作深入底层的研究,我将以一个加载图片的例子来向您展示 Coroutine 的基本使用方法。

# 使用 Coroutine 之前的初始配置

首先我们使用 android studio 新建一个项目,并在新建项目的时候勾选【Include Kotlin support】, 就像下边这样

img

项目创建成功后,我们需要在 build.gradle 文件中的 android 配置模块下面增加如下的配置

kotlin {
    experimental {
        coroutines 'enable'
    }
}

然后在 build.gradle 文件中添加如下的依赖

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20'
 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20'

完整的配置情况如下:

img

经过上边的步骤 Coroutine 的配置就已经完成了。接下来我们就可以使用 Coroutine 了。

# 实现你的第一个 Coroutine 程序

现在我们来开始编写我们的第一个 Coroutine 例子程序,这个程序的主要功能就是从手机媒体中加载一张图片,并把它显示在一个 ImageView 中。我们先来看看在未使用 Coroutine 之前使用同步的方式加载图片的代码如下:

val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)

在上边的代码中我们从媒体读取了一张图片并把它转化成 Bitmap 对象。因为这是一个 IO 操作,如果我们在 UI 主线程中调用这段代码,将可能导致程序卡顿或产生 ANR 崩溃,所以我们需要在新开的线程中调用下边的代码

val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)

接着我们需要在 UI 线程中调用下边的代码来显示加载的图片

imageView.setImageBitmap(bitmap)

为了实现这一功能在传统的 android 程序中我们需要使用 Handler 或 AsyncTask 将结果从非 UI 主线程发送到 UI 主线程进行显示,我们需要编写许多额外的代码。并且这些代码的可读性也不是十分的友好。下边我们来看看使用 Kotlin 的 Coroutine 来实现图片的加载的代码,如下:

val job = launch(Background) {
  val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,uri) 
  launch(UI) {
    imageView.setImageBitmap(bitmap)
  }
}

我们先忽略返回值 job,我们稍后会进行介绍,在这儿我们关心的事情是 launch 函数和参数 Background 与 UI。与之前使用同步的方式加载图片相比唯一的不同就在于这儿我们调用了 lauch 函数。lauch () 创建并启动了一个协程,这儿的参数 Background 是一个 CoroutineContext 对象,确保这个协程运行在一个后台线程,确保你的应用程序不会因耗时操作而阻塞和崩溃。你可以像下边这样定义一个 CoroutineContext:

internal val Background = newFixedThreadPoolContext(2, "bg")

他将使用含有两个线程的线程池来执行协程里边的操作。在第一个协程里边我们又调用了 launch (UI) 创建并启动了一个新的协程,这儿的 UI 并不是我们自己创建的,他是 Kotlin 在 Android 平台里边预定义的一个 CoroutineContext,代表着在 UI 主线程中执行协程里边的操作。所以我们将更新程序界面的操作 imageView.setImageBitmap (bitmap) 放在了这个协程里。通过这儿的例子代码你会发现在 kotlin 里边使用协程来实现线程间的通信和切换非常的简单,比 RxJava 还简单。看上去就跟你写同步的方式的代码一样。

# 取消协程

在上边的例子中我们返回了一个 Job 类型的对象 job。通过调用 job.cancel () 我们能够取消一个协程。例如当我们退出当前 Activity 的时候,图片还没有加载完。这个时候我们就可以在 onDestroy 中调用 job.cancel () 来取消这个未完成的任务。这与我们使用 Rxjava 时调用 dipose () 或使用 AsyncTask 时调用 cancel () 来取消未完成的操作的作用是一样的。

# LifecycleObserver

android 架构组件(Android Architecture Components)里边引入了许多非常好的东西,比如:ViewModel, Room 和 LiveData 以及 Lifecycle API。给予我们一种非常安全简便的方式监听 Activity 和 Fragment 的生命周期变化。接下来我们将使用他们来对之前加载图片的例子进行改进,利用 lifecycle 对 Activity 生命周期进行监听并做出相应的处理(监听到 Activity 调用 onDestroy () 时自动取消后台任务)。

我们定义如下的代码来使用协程:

class CoroutineLifecycleListener(val deferred: Deferred<*>) : LifecycleObserver {
  @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
  fun cancelCoroutine() {
    if (!deferred.isCancelled) {
      deferred.cancel()
    }
  }
}

我们也创建了 LifecycleOwner 的一个扩展函数:

fun <T> LifecycleOwner.load(loader: () -> T): Deferred<T> {
  val deferred = async(context = Background,  start = CoroutineStart.LAZY) {
    loader()
  }
  lifecycle.addObserver(CoroutineLifecycleListener(deferred))
  return deferred
}

在这个函数里边有许多新的东西,即使看上去感到疑惑也不要紧,我们会一步一步的对其进行讲解。我们在所有实现 LifecycleOwner 接口的类中扩展了一个 load 函数。也就是说当我们使用支持库的时候我们可以在 Activity 或 Fragment 中直接调用这个 load 函数(支持库里边的 AppCompatActivity 和 Fragment 实现了 LifecycleOwner 接口)。为了能够在这个函数里边访问 lifecycle 成员添加 CoroutineLifecycleListener 作为一个观察者。

load () 函数使用名为 loader 的 lambda 表达式作为参数 (这个 lambda 表达式返回一个泛型类型 T), 在 load () 函数里边我们调用了名叫 async 的函数,这个函数的作用也是用于创建一个协程。它使用 Background 作为上下文。注意第二个参数 start = CoroutineStart.LAZY。它的意思是不会立即启动一个协程。直到你显示的请求他返回一个值的时候它才会启动,稍后你会看到具体怎样做。这个协程返回了一个 Deferred<T > 对象到调用者。它与我们之前提到的 job 对象是类似的,但是他可以携带一个延迟的值,类似于 JavaScript 中的 Promise 或 Java APIs 中的 Future<T>。

接下来我们定义 Deferred<T> 类 (前面我们在 load 函数中返回的类型) 的一个扩展函数 then (),它也使用一个名叫 block 的 lambda 表达式作为参数。这个 lambda 表达式以 T 类型的对象作为参数。具体代码如下:

infix fun <T> Deferred<T>.then(block: (T) -> Unit): Job {
  return launch(context = UI,parent = this) {
    block(this@then.await())
  }
}

这个函数使用 launch () 创建了另外一个协程,这个新的协程将运行在程序的主线程中。我们在这个新的协程中调用了 then 函数中传入的名叫 block 的 lambda 表达式并使用 await () 函数作为它的参数。await () 是在主线程中调用的,但是他并不会阻塞主线程的执行,它将挂起这个函数,主线程可以继续做其他的事情。当值从其他协程中返回的时候,他将被唤醒并将值从 Deferred 传递到这个 lambda 中。挂起函数(Suspending functions)是协程中最主要的概念。

一旦 Activity 的 onDestroy 方法被调用的时候,我们在 load () 函数中添加的 lifecycle 观察者将会取消第一个协程,也会使第二个协程被取消,避免 block () 被调用。

# Kotlin Coroutine DSL

上边我们定义了两个扩展函数和一个用于取消协程的类,让我们来看看如何使用它们,代码如下:

load {
  MediaStore.Images.Media.getBitmap(contentResolver,uri)
} then {
  imageView.setImageBitmap(it)
}

在上边的代码中我们传递一个 lambda 到 load () 函数中,在这个 lambda 中调用了 loadBitmapFromMediaStore () 函数运行在一个后台进程中。一旦 loadBitmapFromMediaStore () 函数返回 Bitmap,load () 函数将返回 Deferred<Bitmap>。扩展的函数 then () 是被 infix 修饰的,因此当 Deferred<Bitmap > 返回之后我们可以使用上面那种奇特的语法调用它。我们传递到 then () 中的 lambda 将接收到一个 Bitmap 对象。因此我们可以简单的调用 imageView.setImageBitmap (it) 显示这个 Bitmap。

上边的代码可以被应用到任何别的需要使用异步调用并将值转递到主线程的操作中。和 RxJava 这种框架比起来 Kotlin 的协程可能没有它那么强大。但是 Kotlin 的协程可读性更强,也更简单。现在你可以安全的使用它来执行你的异步操作了,再也不用担心内存泄漏的发生了。如下是将上边的代码用于从网络加载数据并显示的例子:

load { restApi.fetchData(query) } then { adapter.display(it) }

以上就是本篇文章所要分享的全部内容,希望能够对你有所帮助。如果你发现文章中有不对的地方也欢迎你帮忙指出,以便我做出及时的更正。

源码地址:https://github.com/chenyi2013/CoroutineDemo
参考文章:
https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md
https://developer.android.com/topic/libraries/architecture/lifecycle.html
https://kotlinlang.org/docs/reference/coroutines.html
https://hellsoft.se/simple-asynchronous-loading-with-kotlin-coroutines-f26408f97f46