破解 Kotlin 协程(8) - Android 篇

关键词:Kotlin 协程 Android Ankojava

Android 上面使用协程来替代回调或者 RxJava 其实是一件很是轻松的事儿,咱们甚至能够在更大的范围内结合 UI 的生命周期作控制协程的执行状态~android

本文涉及的 MainScope 以及 AutoDispose 源码:kotlin-coroutines-androidgit

1. 配置依赖

咱们曾经提到过,若是在 Android 上作开发,那么咱们须要引入github

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutine_version'
复制代码

这个框架里面包含了 Android 专属的 Dispatcher,咱们能够经过 Dispatchers.Main 来拿到这个实例;也包含了 MainScope,用于与 Android 做用域相结合。api

Anko 也提供了一些比较方便的方法,例如 onClick 等等,若是须要,也能够引入它的依赖:安全

//提供 onClick 相似的便捷的 listener,接收 suspend Lambda 表达式
implementation "org.jetbrains.anko:anko-sdk27-coroutines:$anko_version"
//提供 bg 、asReference,还没有没有跟进 kotlin 1.3 的正式版协程,不过代码比较简单,若是须要能够本身改造
implementation "org.jetbrains.anko:anko-coroutines:$anko_version"
复制代码

简单来讲:bash

  • kotlinx-coroutines-android 这个框架是必选项,主要提供了专属调度器
  • anko-sdk27-coroutines 是可选项,提供了一些 UI 组件更为简洁的扩展,例如 onClick,但它也有本身的问题,咱们后面详细探讨
  • anko-coroutines 仅供参考,现阶段(2019.4)因为还没有跟进 1.3 正式版协程,所以在 1.3 以后的版本中尽可能不要使用,提供的两个方法都比较简单,若是须要,可自行改造使用。

协程的原理和用法咱们已经探讨了不少了,关于 Android 上面的协程使用,咱们就只给出几点实践的建议。app

2. UI 生命周期做用域

Android 开发常常想到的一点就是让发出去的请求可以在当前 UI 或者 Activity 退出或者销毁的时候可以自动取消,咱们在用 RxJava 的时候也有过各类各样的方案来解决这个问题。框架

2.1 使用 MainScope

协程有一个很自然的特性能刚够支持这一点,那就是做用域。官方也提供了 MainScope 这个函数,咱们具体看下它的使用方法:异步

val mainScope = MainScope()
launchButton.setOnClickListener {
    mainScope.launch {
        log(1)
        textView.text = async(Dispatchers.IO) {
            log(2)
            delay(1000)
            log(3)
            "Hello1111"
        }.await()
        log(4)
    }
}
复制代码

咱们发现它其实与其余的 CoroutineScope 用起来没什么不同的地方,经过同一个叫 mainScope 的实例启动的协程,都会遵循它的做用域定义,那么 MainScope 的定义时怎样的呢?

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
复制代码

原来就是 SupervisorJob 整合了 Dispatchers.Main 而已,它的异常传播是自上而下的,这一点与 supervisorScope 的行为一致,此外,做用域内的调度是基于 Android 主线程的调度器的,所以做用域内除非明确声明调度器,协程体都调度在主线程执行。所以上述示例的运行结果以下:

2019-04-29 06:51:00.657 D: [main] 1
2019-04-29 06:51:00.659 D: [DefaultDispatcher-worker-1] 2
2019-04-29 06:51:01.662 D: [DefaultDispatcher-worker-2] 3
2019-04-29 06:51:01.664 D: [main] 4
复制代码

若是咱们在触发前面的操做以后当即在其余位置触发做用域的取消,那么该做用域内的协程将再也不继续执行:

val mainScope = MainScope()

launchButton.setOnClickListener {
    mainScope.launch {
        ...
    }
}

cancelButton.setOnClickListener {
    mainScope.cancel()
    log("MainScope is cancelled.")
}
复制代码

若是咱们快速依次点击上面的两个按钮,结果就显而易见了:

2019-04-29 07:12:20.625 D: [main] 1
2019-04-29 07:12:20.629 D: [DefaultDispatcher-worker-2] 2
2019-04-29 07:12:21.046 D: [main] MainScope is cancelled.
复制代码

2.2 构造带有做用域的抽象 Activity

尽管咱们前面体验了 MainScope 发现它能够很方便的控制全部它范围内的协程的取消,以及可以无缝将异步任务切回主线程,这都是咱们想要的特性,不过写法上仍是不够美观。

官方推荐咱们定义一个抽象的 Activity,例如:

abstract class ScopedActivity: Activity(), CoroutineScope by MainScope(){
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}
复制代码

这样在 Activity 退出的时候,对应的做用域就会被取消,全部在该 Activity 中发起的请求都会被取消掉。使用时,只须要继承这个抽象类便可:

class CoroutineActivity : ScopedActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_coroutine)
        launchButton.setOnClickListener {
            launch { // 直接调用 ScopedActivity 也就是 MainScope 的方法
                ...
            }
        }
    }
    
    suspend fun anotherOps() = coroutineScope {
        ...
    }
}
复制代码

除了在当前 Activity 内部得到 MainScope 的能力外,还能够将这个 Scope 实例传递给其余须要的模块,例如 Presenter 一般也须要与 Activity 保持一样的生命周期,所以必要时也能够将该做用域传递过去:

class CoroutinePresenter(private val scope: CoroutineScope): CoroutineScope by scope{
    fun getUserData(){
        launch { ... }
    }
}
复制代码

多数状况下,Presenter 的方法也会被 Activity 直接调用,所以也能够将 Presenter 的方法生命成 suspend 方法,而后用 coroutineScope 嵌套做用域,这样 MainScope 被取消后,嵌套的子做用域同样也会被取消,进而达到取消所有子协程的目的:

class CoroutinePresenter {
    suspend fun getUserData() = coroutineScope {
        launch { ... }
    }
}
复制代码

2.3 更友好地为 Activity 提供做用域

抽象类不少时候会打破咱们的继承体系,这对于开发体验的伤害仍是很大的,所以咱们是否是能够考虑构造一个接口,只要 Activity 实现这个接口就能够拥有做用域以及自动取消的能力呢?

首先咱们定义一个接口:

interface ScopedActivity {
    val scope: CoroutineScope
}
复制代码

咱们有一个朴实的愿望就是但愿实现这个接口就能够自动得到做用域,不过问题来了,这个 scope 成员要怎么实现呢?留给接口实现方的话显然不是很理想,本身实现吧,又碍于本身是个接口,所以咱们只能这样处理:

interface MainScoped {
    companion object {
        internal val scopeMap = IdentityHashMap<MainScoped, MainScope>()
    }
    val mainScope: CoroutineScope
        get() = scopeMap[this as Activity]!!
}
复制代码

接下来的事情就是在合适的实际去建立和取消对应的做用域了,咱们接着定义两个方法:

interface MainScoped {
    ...
    fun createScope(){
        //或者改成 lazy 实现,即用到时再建立
        val activity = this as Activity
        scopeMap[activity] ?: MainScope().also { scopeMap[activity] = it }
    }

    fun destroyScope(){
        scopeMap.remove(this as Activity)?.cancel()
    }
}
复制代码

由于咱们须要 Activity 去实现这个接口,所以直接强转便可,固然若是考虑健壮性,能够作一些异常处理,这里做为示例仅提供核心实现。

接下来就是考虑在哪儿完成建立和取消呢?显然这件事儿用 Application.ActivityLifecycleCallbacks 最合适不过了:

class ActivityLifecycleCallbackImpl: Application.ActivityLifecycleCallbacks {
    ...
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        (activity as? MainScoped)?.createScope()
    }

    override fun onActivityDestroyed(activity: Activity) {
        (activity as? MainScoped)?.destroyScope()
    }
}
复制代码

剩下的就是在 Application 里面注册一下这个监听了,这个你们都会,我就不给出代码了。

咱们看下如何使用:

class CoroutineActivity : Activity(), MainScoped {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        launchButton.setOnClickListener {            
            scope.launch {
                ...
            }
        }
    }
}
复制代码

咱们也能够增长一些有用的方法来简化这个操做:

interface MainScoped {
    ...
    fun <T> withScope(block: CoroutineScope.() -> T) = with(scope, block)
}
复制代码

这样在 Activity 当中还能够这样写:

withScope {
    launch { ... }
}   
复制代码

注意,示例当中用到了 IdentityHashMap,这代表对于 scope 的读写是非线程安全的,所以不要在其余线程试图去获取它的值,除非你引入第三方或者本身实现一个 IdentityConcurrentHashMap,即使如此,从设计上 scope 也不太应该在其余线程访问。

按照这个思路,我提供了一套更加完善的方案,不只支持 Activity 还支持 support-fragment 版本在 25.1.0 以上的版本的 Fragment,而且相似于 Anko 提供了一些有用的基于 MainScope 的 listener 扩展,引入这个框架便可使用:

api 'com.bennyhuo.kotlin:coroutines-android-mainscope:1.0'
复制代码

3. 谨慎使用 GlobalScope

3.1 GlobalScope 存在什么问题

咱们以前作例子常用 GlobalScope,但 GlobalScope 不会继承外部做用域,所以你们使用时必定要注意,若是在使用了绑定生命周期的 MainScope 以后,内部再使用 GlobalScope 启动协程,意味着 MainScope 就不会起到应有的做用。

这里须要当心的是若是使用了一些没有依赖做用域的构造器,那么必定要当心。例如 Anko 当中的 onClick 扩展:

fun View.onClick( context: CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View) -> Unit
) {
    setOnClickListener { v ->
        GlobalScope.launch(context, CoroutineStart.DEFAULT) {
            handler(v)
        }
    }
}
复制代码

也许咱们也就是图个方便,毕竟 onClick 写起来可比 setOnClickListener 要少不少字符,同时名称上看也更加有事件机制的味道,但隐藏的风险就是经过 onClick 启动的协程并不会随着 Activity 的销毁而被取消,其中的风险须要本身思考清楚。

固然,Anko 会这么作的根本缘由在于 OnClickListener 根本拿不到有生命周期加持的做用域。不用 GlobalScope 就没法启动协程,怎么办?结合咱们前面给出的例子,其实这个事儿彻底有别的解法:

interface MainScoped {
    ...
    fun View.onClickSuspend(handler: suspend CoroutineScope.(v: View) -> Unit) {
        setOnClickListener { v ->
            scope.launch {   handler(v)   }
        }
    }
}
复制代码

咱们在前面定义的 MainScoped 接口中,能够经过 scope 拿到有生命周期加持的 MainScope 实例,那么直接用它启动协程来运行 OnClickListener 问题不就解决了嘛。因此这里的关键点在于如何拿到做用域。

这样的 listener 我已经为你们在框架中定义好啦,请参见 2.3。

3.2 协程版 AutoDisposable

固然除了直接使用一个合适的做用域来启动协程以外,咱们还有别的办法来确保协程及时被取消。

你们必定用过 RxJava,也必定知道用 RxJava 发了个任务,任务还没结束页面就被关闭了,若是任务迟迟不回来,页面就会被泄露;若是任务后面回来了,执行回调更新 UI 的时候也会大几率空指针。

所以你们必定会用到 Uber 的开源框架 AutoDispose。它其实就是利用 ViewOnAttachStateChangeListener ,当 View 被拿下的时候,咱们就取消全部以前用 RxJava 发出去的请求。

static final class Listener extends MainThreadDisposable implements View.OnAttachStateChangeListener {
    private final View view;
    private final CompletableObserver observer;

    Listener(View view, CompletableObserver observer) {
      this.view = view;
      this.observer = observer;
    }

    @Override public void onViewAttachedToWindow(View v) { }

    @Override public void onViewDetachedFromWindow(View v) {
      if (!isDisposed()) {
      //看到没看到没看到没?
        observer.onComplete();
      }
    }

    @Override protected void onDispose() {
      view.removeOnAttachStateChangeListener(this);
    }
  }
复制代码

考虑到前面提到的 Anko 扩展 onClick 没法取消协程的问题,咱们也能够搞一个 onClickAutoDisposable

fun View.onClickAutoDisposable ( context: CoroutineContext = Dispatchers.Main, handler: suspend CoroutineScope.(v: View) -> Unit
) {
    setOnClickListener { v ->
        GlobalScope.launch(context, CoroutineStart.DEFAULT) {
            handler(v)
        }.asAutoDisposable(v)
    }
}
复制代码

咱们知道 launch 会启动一个 Job,所以咱们能够经过 asAutoDisposable 来将其转换成支持自动取消的类型:

fun Job.asAutoDisposable(view: View) = AutoDisposableJob(view, this)
复制代码

那么 AutoDisposableJob 的实现只要参考 AutoDisposable 的实现依样画葫芦就行了 :

class AutoDisposableJob(private val view: View, private val wrapped: Job)
    //咱们实现了 Job 这个接口,但没有直接实现它的方法,而是用 wrapped 这个成员去代理这个接口
     : Job by wrapped, OnAttachStateChangeListener {
    override fun onViewAttachedToWindow(v: View?) = Unit

    override fun onViewDetachedFromWindow(v: View?) {
        //当 View 被移除的时候,取消协程
        cancel()
        view.removeOnAttachStateChangeListener(this)
    }

    private fun isViewAttached() =
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null

    init {
        if(isViewAttached()) {
            view.addOnAttachStateChangeListener(this)
        } else {
            cancel()
        }

        //协程执行完毕时要及时移除 listener 省得形成泄露
        invokeOnCompletion() {
            view.removeOnAttachStateChangeListener(this)
        }
    }
}
复制代码

这样的话,咱们就可使用这个扩展了:

button.onClickAutoDisposable{
    try {
        val req = Request()
        val resp = async { sendRequest(req) }.await()
        updateUI(resp)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
复制代码

button 这个对象从 window 上撤下来的时候,咱们的协程就会收到 cancel 的指令,尽管这种状况下协程的执行不会跟随 ActivityonDestroy 而取消,但它与 View 的点击事件紧密结合,即使 Activity 没有被销毁,View 自己被移除时也会直接将监听中的协程取消掉。

若是你们想要用这个扩展,我已经帮你们放到 jcenter 啦,直接使用:

api "com.bennyhuo.kotlin:coroutines-android-autodisposable:1.0"
复制代码

添加到依赖当中便可使用。

4. 合理使用调度器

在 Android 上使用协程,更多的就是简化异步逻辑的写法,使用场景更多与 RxJava 相似。在使用 RxJava 的时候,我就发现有很多开发者仅仅用到了它的切线程的功能,并且因为自己 RxJava 切线程 API 简单易用,还会形成不少无脑线程切换的操做,这样其实是很差的。那么使用协程就更要注意这个问题了,由于协程切换线程的方式被 RxJava 更简洁,更透明,原本这是好事情,就怕被滥用。

比较推荐的写法是,绝大多数 UI 逻辑在 UI 线程中处理,即便在 UI 中用 Dispatchers.Main 来启动协程,若是涉及到一些 io 操做,使用 async 将其调度到 Dispatchers.IO 上,结果返回时协程会帮咱们切回到主线程——这很是相似 Nodejs 这样的单线程的工做模式。

对于一些 UI 不相关的逻辑,例如批量离线数据下载任务,一般默认的调度器就足够使用了。

5. 小结

这一篇文章,主要是基于咱们前面讲了的理论知识,进一步往 Android 的具体实战角度迁移,相比其余类型的应用,Android 做为 UI 程序最大的特色就是异步要协调好 UI 的生命周期,协程也不例外。一旦咱们把协程的做用域规则以及协程与 UI 生命周期的关系熟稔于心,那么相信你们使用协程时必定会驾轻就熟的。


欢迎关注 Kotlin 中文社区!

中文官网:www.kotlincn.net/

中文官方博客:www.kotliner.cn/

公众号:Kotlin

知乎专栏:Kotlin

CSDN:Kotlin中文社区

掘金:Kotlin中文社区

简书:Kotlin中文社区

相关文章
相关标签/搜索