利用Kotlin的协程实现简单的异步加载详解

2020-11-04 12:35:30 浏览数 (1)

前言

众所周知在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】,就像下边这样

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

代码语言:javascript复制
kotlin {
 experimental {
 coroutines 'enable'
 }
}

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

代码语言:javascript复制
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20'
 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20'

完整的配置情况如下:

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

实现你的第一个Coroutine程序

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

代码语言:javascript复制
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)

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

代码语言:javascript复制
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)

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

代码语言:javascript复制
imageView.setImageBitmap(bitmap)

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

代码语言:javascript复制
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:

代码语言:javascript复制
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()时自动取消后台任务)。

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

代码语言:javascript复制
class CoroutineLifecycleListener(val deferred: Deferred<* ) : LifecycleObserver {
 @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
 fun cancelCoroutine() {
 if (!deferred.isCancelled) {
 deferred.cancel()
 }
 }
}

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

代码语言:javascript复制
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类型的对象作为参数。具体代码如下:

代码语言:javascript复制
infix fun <T  Deferred<T .then(block: (T) -  Unit): Job {
 return launch(context = UI) {
 block(this@then.await())
 }
}

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

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

Kotlin Coroutine DSL

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

代码语言:javascript复制
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的协程可读性更强,也更简单。现在你可以安全的使用它来执行你的异步操作了,再也不用担心内存泄漏的发生了。如下是将上边的代码用于从网络加载数据并显示的例子:

代码语言:javascript复制
load { restApi.fetchData(query) } then { adapter.display(it) }

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

源码地址: https://github.com/chenyi2013/CoroutineDemo(本地下载)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对ZaLou.Cn的支持。

参考文章:

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

0 人点赞