使用Kotlin Coroutines进行简单的异步加载

计算机很擅长多任务操作。为了编写出好的软件我们需要对多任务操作和异步有个很好的了解。在Android上面这些包括了activities和fragments的异步的生命周期回调。

Kotlin Coroutines(Kotlin协程)是最近加入到了异步API和库的工具箱中。它不是一个解决所有问题的银弹(a silver bullet),但是在很多情境下它可以让问题变得更简单。本文不会深入探讨coroutines的内部工作原理,而只是举一个怎样在android开发中使用kotlin coroutines的例子。

Let’s get started!

准备,编写Gradle

目前为止Kotlin Coroutines还是实验性的特性,因此使用Kotlin Coroutines需要在app模块的build.gradle添加一些东西,直接在android片段后面加上下面的代码:

kotlin {
    experimental {
        coroutines 'enable'
    }
}

然后再添加两个依赖:

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

你的第一个coroutine

我们的需求是从媒体存储(media storage)中加载一个图片然后通过一个ImageView展示,同步方法可以这样写:

fun loadBitmapFromMediaStore(imageId: Int, imagesBaseUri: Uri): Bitmap {
  val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
  return MediaStore.Images.Media.getBitmap(contentResolver, uri)
}

由于这个方法是IO操作因此必须在后台线程中进行。函数返回Bitmap后我们使用ImageView展示它:

imageView.setImageBitmap(bitmap)

这个调用必须在UI线程否则会crash。只需要三行代码,我们可以这样写:

val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)

取决于加载Bitmap的线程和时间,上面的代码将导致应用程序暂时冻结(糟糕的用户体验)或崩溃。如果使用Kotlin Coroutines我们可以这样写:

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

现在我们先暂时忽略返回值job,等一会再讨论它。重点launch方法以及它的两个参数Background和UI。这段代码和之前的三行代码的不同处在于launch()函数的调用。我们可以很容易地遵循这段代码,它与前面的三行完全同步的代码的的示例几乎完全相同。

函数launch()所做的事情是创建和启动一个coroutine。Background参数是一个CoroutineContext保证这个coroutine运行在后台线程中因此引用不会卡顿或者crash,你可以这样声明一个CoroutineContext:

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

这行代码将会给coroutine创建一个新的context且名叫“bg”,它会使用两个常规线程来执行它的任务。

在第一个协程(launch(Background)创建的)中我们调用了launch(UI),launch(UI)将会出发另一个协程coroutine,这个coroutine运行在预先定义好的使用UI线程的context。这意味着imageView.setImageBitmap()将会运行在UI线程而不会导致应用crash。

取消协程

上面的代码可能是您在使用其他api之前没有做过的。第一个挑战是activity的生命周期问题。如果在加载完成之前我们销毁了activity,那么调用imageView.setImageBitmap()将会导致应用崩溃。为了避免这中情况,我们必须取消这个加载。这个是launch()的返回值需要做的,我们把这个返回值job保存起来,在activity的onStop()中这样做:

job.cancel()

这和RxJava (Disposable调用dispose())或者AsyncTask (调用cancel())所做的事情是一样的。为了执行后台操作而阅读语法,我们并没有获得更多的便利性。我们来看看能否解决这个问题。

生命周期观察者LifecycleObserver

自从支持库(support library)出来之后,Android Architecture Components应该算是Google送给androiders最好的礼物了。有很多的文章来讲解ViewModel、Room和LiveData。另一个伟大的部分是Lifecycle API,利用它我们可以很方便地监听activity和fragment的生命周期变化并作出相应的反应。结合coroutines我们使用下面的代码:

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

我们为LifecycleOwner (FragmentActivity和support Fragment实现了它)创建一个叫做load的扩展函数(我默认并希望你了解kotlin的扩展函数,这是kotlin的基础知识,如果不懂这个基础,那么能懂kotlin coroutines就是奇迹了):

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

  lifecycle.addObserver(CoroutineLifecycleListener(deferred))
  return deferred
}

好吧,我承认这个扩展函数有很多新的东西,我们一点一点来分析。

我们给LifecycleOwner添加了这个扩展函数,而且Activity和Fragment实现了LifecycleOwner,那么在activity和fragment里我们可以直接调用这个load()函数,函数里面我们获取成员变量lifecycle,并添加观察者CoroutineLifecycleListener。

load()函数的参数是一个叫做loader的lambda,这个lambda返回范型T。函数内部,我们调用async()来创建一个coroutine,async()将会运行在后台因为它的参数是Background coroutine context,需要注意的是async()还有第二个参数:start = CoroutineStart.LAZY,这表示这个coroutine不会启动直到有人显示地请求它返回值,后面内容你将会看到怎么使用它。

这个coroutine返回一个Deferred对象给调用者,它和之前的job变量很像,但是它可以携带deferred值比如一个JavaScript Promise或者常规Java APIs中的Future,好处是它可以有一个工作在coroutines中的await()方法,马上你就可以看到。

下面我们给Deferred定义另一个扩展函数then(),而Deferred正是上一个扩展函数返回类型。它也接受一个lambda参数block,block使用一个T类型的对象作为参数并返回Unit。

infix fun  Deferred.then(block: (T) -> Unit): Job {
  return launch(context = UI) {
    block([email protected]())
  }
}

这个函数使用launch()创建一个运行在UI线程的coroutine。block的lambda表达式传递给这个coroutine,它把完成的Deferred对象最为自己的参数。我们调用await()方法来暂停当前coroutine直到Deferred对象返回了值。

需要注意的是这个扩展函数使用了中缀符号infix,如果你不懂中缀符号,下面的对then函数的调用你可能不太明白是咋回事,我在这里简单地说明下,正常的函数调用是object.function(parameter),如果函数是infix函数,可以使用这样的调用方式:object function parameter,比如kotlin标准库的add函数就是infix函数,我们就可以这样调用add函数:1 add 2等价于1.add(2),大致是这么回事,有意见请留言。

这正是kotlin coroutines的迷人之处。await()的调用虽然是在UI线程完成的,但是它并不会阻塞UI线程。它只是暂停函数的执行直到准备好,Deferred对象传递给lambda后它就会唤起并开始执行。当这个coroutine 暂停的时候,UI线程可以继续执行其它的事情。Suspending functions是kotlin coroutines的核心概念和奇迹所在。

load()函数中添加的生命周期观察者(lifecycle observer)在activity的onDestroy()中会cancel掉第一个coroutine,这样会导致第二个coroutine也会被cancel掉因此避免block()执行。

Kotlin Coroutine DSL(Domain Specific Language,领域专用语言)

这两个扩展函数考虑到了coroutine的取消问题,下面看下我们的代码:

load {
  loadBitmapFromMediaStore(imageId, imagesBaseUri)
} then {
  imageView.setImageBitmap(it)
}

上面的代码我们给第一个扩展函数load()传递一个lambda表达式,这个lambda调用了必须运行在后台线程的loadBitmapFromMediaStore()方法。lambda返回Bitmap类型,因此load()扩展函数返回Deferred类型。

上面代码对第二个扩展函数then()的调用看起来很玄幻,这是中缀符号infix特有的调用方式。传递给then()的lambda接收一个Bitmap,因此我们可以调用imageView.setImageBitmap(it)方法。多谢生命周期观察者(lifecycle observer),取消(Cancellation)这个问题我们也考虑到了。

上面的代码对于这种异步调用的情景是通用的:首先从后台线程获取数据,然后在UI线程展示数据。貌似kotlin coroutines不像RxJava那样强大,因为RxJava可以处理多个调用,但是kotlin coroutines更简单易读并且覆盖了大部分的应用场景。你可以写出这样安全的代码,而不必担心泄漏Context或者在每次调用中处理线程:

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

load()then()代码如此简短而不足以搞一个新的library,但是我希望将来一旦kotlin coroutines有了稳定的正式版本,在Kotlin-based library中能够出现类似的东西。

到目前为止,你有两个选择,既可以采用上面的简单代码也可以看下Anko Coroutines。在这里我还发布了一个更加完整的版本。祝你在kotlin coroutines的冒险中旅途愉快!


原文地址,翻译的不是很好,大致只翻译了技术部分,一些啰嗦的段落和句子没有翻译,有好的意见请留言。原文的第二个扩展函数then()有bug,具体bug和bugfix请看原文的两条评论:评论1和评论2。

你可能感兴趣的:(使用Kotlin Coroutines进行简单的异步加载)