使用Kotlin的协程实现简单的异步加载

众所周知在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配置模块下面增加如下的配置

kotlin {
    experimental {
        coroutines 'enable'
    }
}

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

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

完整的配置情况如下:


经过上边的步骤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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容