Kotlin协程及在Android中的应用

什么是协程

协程并不是kotlin中才有的概念,在Python、Go等中也有协程。初学kotlin协程对这个概念很迷惑,比如官方文档上说:协程是一种轻量级的线程,我们很容易将协程和线程联系起来产生误解,认为协程也是一种线程。其实协程的实现也是离不开线程的,它也是跑在线程中,可以是单线程也可以是多线程,简单来说协程就是一个线程的框架。

我们可以在实际的开发中去理解它,在android中避免不了要使用网络请求数据,网络请求必须要在子线程中执行,获得的数据必须要在主线程更新UI,这就一定会在子线程和主线程之间来回切换,在最初我们都是通过Handler或AsyncTask来实现,当接口多或逻辑复杂的时候你会觉得这种方式很痛苦,一层层的嵌套回调,这就变成了我们所说的回调地狱,不仅维护起来很麻烦。

Rxjava框架的出现解决了这类的问题,它的链式结构简化了逻辑解耦了各个模块的操作,各个模块可以随意组合切换可以很好的消除回调,最方便的是可以轻松的切换线程。

使用协程同样可以像Rxjava那样有效的消除回调地狱,不过它们的设计理念和代码风格都是有很大区别的,写法和写同步代码一样。

基本使用

添加依赖

buildscript {
   ext.kotlin_version = '1.3.31'
}
dependencies{
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_version'
   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_version'
}

开启一个协程

GlobalScope.launch() {
   //do something
}

上面代码就是协程的简单实现,launch方法默认有三个参数,分别是上下文CoroutineContext启动模式协程体CoroutineContext是一个接口,调度器、拦截器都是它的子类。

启动模式

协程的启动模式分为四种,分别是 DEFAULT,LAZY,ATOMIC,UNDISPATCHED,最常用的是DEFAULT,LAZY

  • DEFAULT 默认模式,立即执行协程体

    fun main() {
        runBlocking {
            defaultStart()
        }
    }
    private suspend fun defaultStart() {
       GlobalScope.launch() {
            println(Thread.currentThread().name)
        }
    }
    

    打印日志:

    DefaultDispatcher-worker-1

    DEFAULT模式是默认的启动模式,不需要手动调用join方法,当调用时会自动执行协程体

  • LAZY模式,懒加载模式,需要手动调用join方法

    private suspend fun lazyStart() {
        val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
            println(Thread.currentThread().name)
        }
        job.join()
    }
    

    打印日志:

    DefaultDispatcher-worker-1

    • ATOMIC模式,会立即执行协程体,和cancel有关,调用 cancel 的时机不同,结果也是有差异的。
    private suspend fun atomicStart() {
        val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
            println("1: " + Thread.currentThread().name)
            delay(1000)
            println("2: " + Thread.currentThread().name)
        }
    		//ATOMIC启动模式的协程体 即使调了cancel方法 也一定会执行
        job.cancel()
        delay(2000)
    }
    

    打印日志:

    DefaultDispatcher-worker-1

    这里不会打印 2 出来,因为调用job.cancel()会导致JobCancellationException异常,当执行到delay挂起函数后会判断协程是处于cancel状态就不会再往下执行,我们可以try-catch delay函数就会执行下面的代码了。

    private suspend fun atomicStart() {
        val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
            println("1: " + Thread.currentThread().name)
            try {
                //JobCancellationException,不捕获异常则不会执行下面代码
                delay(1000)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            println("2: " + Thread.currentThread().name)
        }
        // ATOMIC启动模式的协程体 即使调了cancel方法 也一定会执行
        job.cancel()
        delay(2000)
    }
    

    打印日志:

    1: DefaultDispatcher-worker-1
    kotlinx.coroutines.JobCancellationException: Job was cancelled; job=StandaloneCoroutine{Cancelling}@68d421ba
    2: DefaultDispatcher-worker-1

  • UNDISPATCHED模式,协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点。

    fun main() {
        runBlocking {
            undispatchedStart(this)
        }
    }
    private suspend fun undispatchedStart(coroutineScope: CoroutineScope) {
        println("0: " + Thread.currentThread().name)
        coroutineScope.launch(context = Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) {
            println("1: " + Thread.currentThread().name)
            delay(1000)
            println("2: " + Thread.currentThread().name)
        }
        delay(2000)
    }
    

    打印日志:

    0: main
    1: main
    2: DefaultDispatcher-worker-1

    从打印日志可以看出使用UNDISPATCHED模式默认在当前的线程中执行,因此0和1都是在主线程中执行,当与到delay函数时就会切换到另外一个线程,切换到哪个线程取决于当前的调度器。上面代码中使用的是默认的调度器,因此2是在子线程中执行。

上下文

前面我们提到launch第一个参数是上下文CoroutineContext,下面看一下CoroutineContext的代码。

public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else 
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }
    public fun minusKey(key: Key<*>): CoroutineContext
    public interface Key<E : Element>
    public interface Element : CoroutineContext {
        public val key: Key<*>
        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null
        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)
        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}

CoroutineContext是一个数据结构,类似一个单链表,主要有三个方法get获取元素、minusKey移除元素、plus添加元素。最重要的是plus方法,这个方法被operator修饰,这是kotlin中的操作符重载,可以通过 + 来调用plus方法。例如需要多个上下文:

 GlobalScope.launch(Dispatchers.Default + CoroutineName("Jack")) {
   //do something
 }

拦截器

协程拦截器也是上下文子类,所以也可以用 + 来操作,了解Okhttp的源码的肯定知道Okhttp也有拦截器,通过拦截器可以实现加解密、打印日志、缓存等功能,kotlin协程也是一样的道理。

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    public fun releaseInterceptedContinuation(continuation: Continuation<*>) {}
    public override operator fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? =
        @Suppress("UNCHECKED_CAST")
        if (key === Key) this as E else null
    public override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext =
        if (key === Key) EmptyCoroutineContext else this
}

调度器

协程的调度器主要是用来指定协程代码在哪个地方执行或将它分配到一个线程池,主要有四个默认调度器,分别是DefaultMainUnconfinedIO(jvm版本)。

jvmjsNative
Default线程池主线程循环主线程循环
MainUI线程主线程循环主线程循环
Unconfined直接执行直接执行直接执行
IO线程池--
  • IO调度器只在jvm中有定义,它和Default默认基于线程池
  • Main调度器是在主线程中运行,比如在Android中会切换到UI线程中执行
  • Js是单线程的事件循环,与 Jvm 上的 UI 程序比较类似
Dispatchers.Default、Dispatchers.IO
fun main() {
    runBlocking {
        defaultDispatcher()
    }
}
private suspend fun defaultDispatcher() {
    GlobalScope.launch(Dispatchers.Default) {
        println("1: " + Thread.currentThread().name)
        launch(Dispatchers.IO) {
            delay(1000) // 延迟1秒后,再继续执行下面的代码
            println("2: " + Thread.currentThread().name)
        }
        println("3: " + Thread.currentThread().name)
    }

    delay(2000)
}

打印日志:

1: DefaultDispatcher-worker-1
3: DefaultDispatcher-worker-1
2: DefaultDispatcher-worker-2

由此可见launch默认的调度器是Dispatchers.Default,它和Dispatchers.IO一样都是在子线程中执行

Dispatchers.Unconfined

Unconfined拦截器就是没有指定协程在哪个线程中执行,也就是当前在哪个线程接下来的代码就在哪个线程中执行,比如当前在主线程main中接下来一定会在主线程中执行。

fun main() {
    runBlocking {
        defaultDispatcher()
    }
}
private suspend fun defaultDispatcher() {
    GlobalScope.launch(Dispatchers.Unconfined) {
        println("1: " + Thread.currentThread().name)
        launch(Dispatchers.IO) {
            delay(1000) // 延迟1秒后,再继续执行下面的代码
            println("2: " + Thread.currentThread().name)
        }
        println("3: " + Thread.currentThread().name)
    }

    delay(2000)
}

打印日志:

1: main
3: main
2: DefaultDispatcher-worker-1

从上面代码可以看出,在main函数中是主线程因此协程代码也是在主线程中执行。

调度器自定义线程

我们可以自定义线程绑定掉协程的调度器,但是这种方式不建议使用,因为一旦手动创建了线程就必须要手动close,否则线程就永远也不会终止,这样会很危险

val dispatcher = Executors.newSingleThreadExecutor { r -> Thread(r, "custom thread") }.asCoroutineDispatcher()
private suspend fun customDispatcher() {
    GlobalScope.launch(dispatcher) {
        println("1: " + Thread.currentThread().name)
        delay(1000)
        println("2: " + Thread.currentThread().name)
    }

    delay(2000L)
    // 一定要close,否则线程永远都不会结束,很危险
    dispatcher.close()
}

打印日志:

1: custom thread
2: custom thread

Process finished with exit code 0

作用域

协程的作用域就是协程运行的有效范围,比如说我在一个方法有一个变量,那么这个方法就是这个变量的作用域,协程作用域也是这样一个作用,可以用来确保里面的协程都有一个作用域的限制。

GlobeScope

GlobeScope作用域启动的协程会单独启动一个新的作用域,其内部子协程都是遵循它的作用域规则,一般不建议使用这个作用域,不利于维护。

coroutineScope

coroutineScope会继承父协程的作用域,取消操作和产生异常都是双向传播的,比如子协程产生取消后会通知父协程取消。

supervisorScope

supervisorScope也是会继承父协程的作用域,它与coroutineScope不一样的是取消操作和产生异常都是单向传播的,也就是子协程取消或产生异常并不会影响到父协程,只会影响到它自己和自己的子协程。

我们可以执行以下代码验证coroutineScope和supervisorScope区别:

fun main() {
    runBlocking {
      coroutineScope()
      //supervisorScope1()
      //supervisorScope2()
    }
}
//coroutineScope模式(父协程异常)
private suspend fun coroutineScope(){
    GlobalScope.launch {
        println("1:"+ Thread.currentThread().name)
        coroutineScope {
            println("2:"+ Thread.currentThread().name)
            //启动一个子协程
            launch {
                try {
                    delay(1000)
                    println("3:"+ Thread.currentThread().name)
                } catch (e: Exception) {
                    println("error")
                }
            }
            delay(100)
            1/0 //父协程报错
            println("4:"+ Thread.currentThread().name)
        }
    }
    delay(2000)
}
//supervisorScope模式(子协程异常)
private suspend fun supervisorScope1(){
    GlobalScope.launch {
        println("1:"+ Thread.currentThread().name)
        supervisorScope {
            println("2:"+ Thread.currentThread().name)
            // 启动一个子协程
            launch {
                1/0
            }
            delay(100)
            println("3:"+ Thread.currentThread().name)
        }
        println("4:"+ Thread.currentThread().name)

    }
    delay(2000)
}
//supervisorScope模式(父协程异常)
private suspend fun supervisorScope2(){
    GlobalScope.launch {
        println("1:"+ Thread.currentThread().name)
        supervisorScope {
            println("2:"+ Thread.currentThread().name)
            // 启动一个子协程
            launch {
                try {
                    delay(1000)
                    println("3:"+ Thread.currentThread().name)
                } catch (e: Exception) {
                    println("error")
                }
            }
            delay(100)
            1/0 //父协程报错
            println("4:"+ Thread.currentThread().name)
        }
    }
    delay(2000)
}

协程在Android中的应用

现在app的网络请求基本是Okhttp+Retrofit的天下了,在Retrofit2.6.0版本支持了协程,现在我们看下Retrofit基于协程写网络请求的方式。

添加依赖:

implementation "com.squareup.retrofit2:retrofit:2.6.0"
implementation "com.squareup.retrofit2:converter-gson:2.6.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.6.0"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"

创建ApiService:

val apiService = Retrofit.Builder()
			.baseUrl("https://api.github.com/")
      .client(RRetrofit.provideOkhttpClientBuilder().build())
      .addConverterFactory(GsonConverterFactory.create())
      //添加对 Deferred 的支持
      .addCallAdapterFactory(CoroutineCallAdapterFactory.invoke())
      //添加对 RxJava2 的支持
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .build()
			.create(ApiService::class.java)

ApiService:

//Rxjava使用方式
@GET("users/{login}")
fun getUser1(@Path("login") name: String): Observable<User>

//Retrofit 2.4.0新增使用方式
@GET("users/{login}")
fun getUser2(@Path("login") name: String): Deferred<User>

//Retrofit 2.6.0新增使用方式
@GET("users/{login}")
suspend fun getUser3(@Path("login") name: String): User

从上面代码可以看出api使用协程有两种方式,一种返回Deferred,使用的时候需要调用await()方法获取User对象,另一种是在方法前加suspend标记,返回值直接是User。

private fun request() {
   GlobalScope.launch {
       withContext(Dispatchers.Main) {
         try {
             val user = api.getUser2("zhangyujiu").await()
             Loger.e(user.toString())
            } catch (e: Exception) {
          			e.printStackTrace()
            }
        }
    }
    GlobalScope.launch {
        withContext(Dispatchers.Main) {
          try {
              val user = api.getUser3("zhangyujiu")
              Loger.e(user.toString())
             } catch (e: Exception) {
                e.printStackTrace()
             }
         }
    }
}

MainScope

在前面我们都是使用的GlobalScope.launch启动的协程,它会创建一个新的作用域并且不会继承外部作用域,还有一个更大的问题是当发送请求后退出当前页面没有自动取消请求,因此不建议使用GlobalScope.launch启动协程,官方建议我们使用MainScope

使用方式:

BaseActivity实现CoroutineScope接口然后使用类委托委托给MainScope

open class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope(){
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

查看MainScope的代码,其实就是SupervisorJob+Dispatchers.Main调度器,SupervisorJob类似于supervisorScope作用域,因此我们的Activity就是一个作用域,在需要启动协程是直接使用launch{}。

前面的request方法可以直接使用launch启动:

private fun request() {
   launch {
       withContext(Dispatchers.Main) {
         ......
       }
    }
}

viewModelScope、lifecycleScope

Android官方对Jetpack的ViewModel和Lifecycle组件提供了绑定生命周期作用域,我们可以直接使用,不需要手动cancel,我们需要添加以下依赖:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"

使用方式:

viewModelScope.launch {
		......
}
lifecycleScope.launch {
		......
}

viewModelScope作用域用CloseableCoroutineScope包裹住保存在ViewModel的mBagOfTags中,当关闭页面后ViewModel会调用clear方法,clear方法会调用closeWithRuntimeException方法,最后调用close方法后关闭协程作用域,因此我们不用手动调用cancel方法。

private static void closeWithRuntimeException(Object obj) {
        if (obj instanceof Closeable) {
            try {
                ((Closeable) obj).close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
    }
}

lifecycleScope的自动取消更加简单,lifecycle是有对宿主生命周期感知能力的组件,在LifecycleCoroutineScopeImpl中实现LifecycleEventObserver接口并注册lifecycle,当宿主生命周期变为Lifecycle.State.DESTROYED时调用cancel。

internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }
    fun register() {
        launch(Dispatchers.Main.immediate) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
}

AutoDisposable

除了上面的作用域可以确保协程自动取消,我们也可以自己实现,这里可以参考bennyhuo大佬的方式来实现,具体请查看 AutoDisposable

使用方式:

GlobalScope.launch(context, CoroutineStart.DEFAULT) {
     ......
}.asAutoDisposable(view)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值