Kotlin协程-Android实战

2,315 阅读10分钟

协程是什么?

​ 协程的概念最核心的点其实就是函数或者一段程序能够被挂起(说暂停其实也没啥问题),待会儿再恢复,挂起和恢复是开发者的程序逻辑自己控制的,协程是通过主动挂起出让运行权来实现协作的(本质上是通过回调来实现的)。它跟线程最大的区别在于线程一旦开始执行,从任务的角度来看,就不会被暂停,直到任务结束这个过程都是连续的,线程之间是抢占式的调度,因此也不存在协作问题。

JVM中的协程是只是一个线程框架吗?Kotlin 协程确实在实现的过程中提供了很方便切线程的能力,但这不是它的身份,只是它的一种能力而已。

协程相对于Handler,RxJava有什么优点

  • 可以很方便的做到发起和取消任务
  • 可以把运行在不同线程的代码放到一个代码块中。用看起来同步的方式写异步代码,本质上内部还是通过回调来实现的

协程的启动

可以通过launch() : Jobasync(): Deferred 来启动一个协程。DeferredJob的子类,可以通过Job对象进行取消CancelJoin(挂起主协程,直到当前协程执行完成)操作。通过Deferred可以得到协程执行结果,一般用于网络请求等操作

// GlobalScope 启动一个最外部全局协程 是一个单例对象
GlobalScope.lauch() {
   val launch = launch {
       // 启动二级协程
       repeat(100) {
           println("挂起中$it")
           delay(500)
       }
   }
   launch.join() // join方法,挂起当前主协程 直到launch协程执行完
   delay(1600)   // 挂起主协程 1600ms
   launch.cancel() //  cancel() 取消launch协程
    
   // Deferred是Job的子类,多了await()获取执行结果 
   var job2: Deferred<String> = async {
       delay(500);
       return@async "hello"
   }    
   println("async执行结果:" + job2.await()) //await为获取async协程执行结果
}

使用async并发

async一般用于同时发起多个耗时请求,多个耗时请求的结果作用于同一个对象。打个比方,股票的报价和name通过两个接口获取,但是需求需要同时展示出来,使用async就很方便处理

suspend fun getPrice(): Int {
    delay(1000L) // 模拟接口耗时
    return 198
}

suspend fun getName(): String {
    delay(1000L) // 模拟接口耗时
    return "AAPL"
}

//measureTimeMillis测量耗时
val time = measureTimeMillis {
    val one:Deferred<Int> = async { getPrice() } // 已经在执行内容
    val two:Deferred<String> = async { getName() } // 已经在执行内容
    // await() 获取执行结果
    println("name: ${two.await()}  price: ${one.await()}") 
    // name: AAPL  price: 198
}
println("Completed in $time ms") //Completed in 1017L ms

以上demo中,可以看到总耗时只有1000多点,在two.await()虽然会阻塞当前协程,但是在await()前,两个async已经开始执行,two.await()阻塞1000L后,执行one.await()结果已经返回,不需要再等到1000L。

惰性启动的async

coroutine一个可选的参数start并传值CoroutineStart.LAZY,可以对协程进行惰性操作。当一个start函数被调用,协程才会被启动。

fun main() = runBlocking<Unit> {
    val measureTimeMillis = measureTimeMillis {
        val one: Deferred<Int> = async(start = CoroutineStart.LAZY) { doSomethingOne() }
        val two: Deferred<Int> = async(start = CoroutineStart.LAZY) { doSomethingTwo() }
        one.start()
        two.start()
        println("The value is ${one.await() + two.await()}")
    }
    println("TimeMillis $measureTimeMillis")
}

协程调度器CoroutineDispater

CoroutineDispater本身是协程上下文CoroutineContext的子类,同时实现了拦截器的接口。协程调度器的作用是将协程的执行局限在指定的线程

Dispatchers.Default // 在线程池中运行
Dispatchers.IO // 在线程池中运行,基于Default调度器背后的线程池,并实现了独立的队列和限制
Dispatchers.Main // 在主线程中运行,Android中为UI线程
Dispatchers.Unconfined // 在调用者线程直接启动
newSingleThreadContext("MyNewThread") // 新创建一个MyNewThread线程 在该线程中运行

通过 withContext 挂起协程并更改协程的执行线程,实现方便的线程切换。可以有返回值。

fun main() = runBlocking {
         launch (Dispatchers.IO) {
              // 模拟耗时操作
              val text = getText()
              withContext(Dispatchers.Main) {
                  // 模拟View展示内容
                  print(text)
              }
         }
         
         launch (Dispatchers.Main) {
              // 模拟耗时操作
              val text = withContext(Dispatchers.IO){
                 getText()
              }
              // 模拟View展示内容
              print(text)
         }
    }

协程的取消

通过Job对象的cancel()来取消协程,耗时挂起函数(比如delay) 需要加try catch保护 不然cancel时则会抛出异常。协程中的轮询代码必须可配合isActive取消

fun main(args: Array<String>) = runBlocking<Unit> {
    val job = launch {
        println("start")
        try {
            delay(1000L)
        } catch (e:Exception) {
            println("error")
        }
        println("1")
        // isActive是协程内CoroutineScope对象的的一个扩展属性,判断协程是否可用,检查当前协程是否可以取消
        while (isActive) {
           // ...    
        }

        if (isActive) {
            println("2")
        }
        println("3")
        delay(1000L)
        println("end")
    }
    delay(100L)
    job.cancelAndJoin() // 取消任务,并等待其结束
}

/// 打印
// start
// error
// 1
// 3

如果执行到 协程体内的代码需要依赖协程的cancel状态(比如delay方法),则会抛出异常。

如果协程体内的代码不依赖协程的cancel状态(比如println方法),则会继续往下执行 。也就是说协程的取消(cancel) 导致协程体终止运行的方式是抛出异常,如果协程体的代码不依赖协程的cancel状态(即不会报错),则协程的取消 对协程体的执行一般没什么影响

作用域构建器coroutineScope

使用coroutineScope会构建一个新的协程作用域,其作用域属于上级协程,内部协程的取消和异常传播都是双向传播 ,coroutineScoperunBlocking很相似,他们都会挂起阻塞当前协程,但是coroutineScope不会像runBlocking那样阻塞线程。

fun main() = runBlocking<Unit> {
    // coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
    coroutineScope {
        var job = launch {
            delay(500L)
            println("Task in coroutineScope")
        }
        delay(100L)
        println("Task from coroutine scope")
    }
    println("主协程结束")
}
// Task from coroutine scope
// Task in coroutineScope
// 主协程结束

协程的几个作用域

  • GlobalScope 启动的协程会单独启动一个作用域,其内部的子协程遵从默认的作用域规则,父协程向子协程单向传递,不能从子协程传向父协程
  • coroutineScope 启动的协程会继承父协程的作用域,其内部的取消操作是双向传播的,协程未捕获的异常也会向上传递给父协程,也会向下传递给子协程。
  • supervisorScope 启动的协程会继承父协程的作用域,他跟coroutineScope不一样的点是 它是由父协程向子协程单向传递的,即内部的取消操作和异常传递 只能由父协程向子协程传播,不能从子协程传向父协程 MainScope 就是使用的supervisorScope作用域,所以只需要子协程 出错 或 cancel 并不会影响父协程,从而也不会影响兄弟协程

协程异常传递模式

协程的异常传递跟协程作用域有关,要么跟coroutineScope一样双向传递,要么跟supervisorScope一样父协程向子协程单向传递

coroutineScope的双向传递

fun main() = runBlocking<Unit> {
    // coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
    try {
        coroutineScope {
            println("start")
            launch {
                // coroutineScope内部协程报错会传递给外面协程
                throw NullPointerException("null")
            }
            delay(100L)
            println("end")
        }
    } catch ( e: Exception) {
        println("error")
    }
}
// start
// error


fun main() = runBlocking<Unit> {
    // coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
    try {
        coroutineScope {
            println("start")
            launch {
               try {
                   delay(1000L)
                   println("launch")
               } catch (e: Exception) {
                   println("inner error")
               }
            }
            delay(100L)
            throw NullPointerException("null")
            println("end")
        }
    } catch ( e: Exception) {
        println("out error")
    }
}

// start
// inner error
// out error

coroutineScope作用域中的子协程 如果出现了异常,会传递给父协程。 父协程如果出现了传递也会传递给子协程,并且还会向外层协程传递。这种传递方式不便于管理,不建议使用

supervisorScope单向传递

fun main() = runBlocking<Unit> {
    // coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
       try {
           supervisorScope {
               println("start")
               launch {
                   throw NullPointerException("ddd")
               }
               delay(100L)
               println("end")
           }
       } catch ( e:Exception ) {
           println("error")
       }
}
// start
// Exception in thread "main" java.lang.NullPointerException: ddd
// end

子协程的异常并不会向外层协程传递,并不会影响父协程的执行

挂起函数

suspend修饰的新函数,就是挂起函数。挂起函数只能在协程或者挂起函数中被调用,就像使用普通函数一样,但是它们有额外的特性——可以调用其他的挂起函数去挂起协程的执行。挂起的本质就是回调

delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会挂起协程,并且只能在协程获取其他挂起函数中使用。

非阻塞式:用看起来阻塞的代码写出非阻塞的操作

fun main(args: Array<String>) = runBlocking<Unit> { 
    val job = launch { doWorld() } 
    println("Hello,") 
    job.join() 
} // 这是你第一个挂起函数 

suspend fun doWorld() 
    //比如在挂起函数doWorld中可以调用其他挂起函数delay,去挂起协程的执行
    delay(1000L)
    println("World!") 
}

把回调切换成挂起函数

suspendCoroutine这个方法并不是帮我们启动协程的,它运行在协程当中并且帮我们获取到当前的协程Continuation实例,也就是拿到回调。方便后面我们调用它的 resume 来返回结果或者 resumeWithException 或者抛出异常。

通过 suspendCoroutine 可以实现把网络请求回调代码 转换成 挂起函数的方式

GlobalScope.launch(Dispatchers.Main) {
     userNameView.text = getUserCoroutine().name
}

suspend fun getUserCoroutine() = suspendCoroutine<User> { continuation ->
    getUser(object : Callback<User> {
        override fun onSuccess(value: User) {
            continuation.resume(value)
        }

        override fun onError(t: Throwable) {
            continuation.resumeWithException(t)
        }
    })
}

fun getUser(callback: Callback<User>) {
    val call = OkHttpClient().newCall(
            Request.Builder()
                    .get().url("https://api.github.com/users/bennyhuo")
                    .build())

    call.enqueue(object : okhttp3.Callback {
        override fun onFailure(call: Call, e: IOException) {
            callback.onError(e)
        }

        override fun onResponse(call: Call, response: Response) {
            response.body()?.let {
                try {
                    callback.onSuccess(User.from(it.string()))
                } catch (e: Exception) {
                    callback.onError(e) // 这里可能是解析异常
                }
            }?: callback.onError(NullPointerException("ResponseBody is null."))
        }
    })
}

如果想通过取消协程来实现取消网络请求,那么就需要使用suspendCancellableCoroutine可以设置一个协程取消的回调,在回调中取消网络请求

suspend fun getUserCoroutine() = suspendCancellableCoroutine<User> { continuation ->
    val call = OkHttpClient().newCall(
            Request.Builder()
                    .get().url("https://api.github.com/users/bennyhuo")
                    .build())
    
    continuation.invokeOnCancellation { 
        log("invokeOnCancellation: cancel the request.")
        call.cancel()
    }

    call.enqueue(object : okhttp3.Callback {
        override fun onFailure(call: Call, e: IOException) {
            continuation.resumeWithException(t)
        }

        override fun onResponse(call: Call, response: Response) {
            response.body()?.let {
                try {
                    continuation.resume(User.from(it.string()))
                } catch (e: Exception) {
                    continuation.resumeWithException(e) // 这里可能是解析异常
                }
            } ?: continuation.resumeWithException(NullPointerException("ResponseBody is null."))
        }
    })                            
}

协程的通信Channel

Channel主要用于协程之间的通信。Channel是一个和阻塞队列BlockingQueue非常相似的概念。

fun main(args: Array<String>) = runBlocking<Unit> {
        val channel = Channel<Int>()
        launch {
            put(channel)
        }
        launch {
            get(channel)
        }
    }
    
    suspend fun put(channel: Channel<Int>) {
        var i = 0
        while (true) {
            channel.send(i++)
        }
    }
    
    suspend fun get(channel: Channel<Int>) {
        while (true) {
            println(channel.receive())
        }
    }

Channel可以通过close关闭来表示没有更多的元素进入通道。使用for循环从通道中接收元素,如果不使用close指令,那么接收协程会一直被挂起

//创建缓冲个数为5的Channel通道
val channel = Channel<Int>(5)
launch {
   for (x in 1..5) channel.send(x * x)
   channel.close() // 我们结束发送
}
// 这里我们使用 `for` 循环来打印所有被接收到的元素(直到通道被关闭)
for (y in channel) 
   println(y)
println("Done!")

produce函数

一般不如上直接使用Channel,而是使用 produce协程函数。Channel拥有缓存对象,可以设置缓存个数。当Channel中缓存满时,再调用send函数时会将当前协程挂起。

fun main(args: Array<String>) = runBlocking<Unit> {
        val product:ReceiveChannel<Int> = put()
        get(product).join()
        delay(2000L)
        product.cancel()
    }
    
    //produce协程函数返回得到一个Channel capacity = 5声明Channel缓存个数为5
    fun CoroutineScope.put() = produce(capacity = 5, context = CommonPool) {
        var i = 0
        while (isActive) {
            //channel的值等于缓存个数的时候 调用send方法会被挂起
            send(i++)
        }
    }
    
    fun CoroutineScope.get(channel: ReceiveChannel<Int>) = launch {
        channel.take(10).consumeEach {
            println("接受到$it")
        }
    }

Android开发中使用协程

首先要实现 CoroutineScope 这个接口,CoroutineScope 的实现教由代理实例 MainScope()

MainScope()的返回实例是CoroutineScope的子类,声明了contextDispatchers.Main+SupervisorJob。就是 SupervisorJob 整合了 Dispatchers.Main 而已,它的异常传播是自上而下的,这一点与 supervisorScope 的行为一致,此外,作用域内的调度是基于 Android 主线程的调度器的,因此作用域内除非明确声明调度器,协程体都调度在主线程执行。

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //加载并显示数据
        loadDataAndShow()
    }
    
    private fun loadDataAndShow(){
        launch(Dispatchers.IO) {
            //IO 线程里拉取数据
            var result = fetchData() 

            withContext(Dispatchers.Main){
                //主线程里更新 UI
                text.setText(result)
            }
        }
    }

    suspend fun fetchData():String{
        delay(2000)
        return "content"
    }

    override fun onDestroy() {
        super.onDestroy()
        // 触发作用域协程的取消,那么该作用域内的协程将不再继续执行:
        cancel()
    }
}

retrofit2对协程的支持

retrofit2 2.6.0版本以上才开始支持协程

data class Repository(val id: Int,
                      val name: String,
                      val html_url: String)

interface SplashInterface {
    // 正常的Call返回
    @GET("/repos/{owner}/{repo}")
    fun contributors(@Path("owner") owner: String,
                     @Path("repo") repo: String): Call<Repository>

    // 协程的Deferred
    @GET("/repos/{owner}/{repo}")
    fun contributors2(@Path("owner") owner: String,
                     @Path("repo") repo: String): Deferred<Repository>

    // 协程的suspend
    @GET("/repos/{owner}/{repo}")
    suspend fun contributors3(@Path("owner") owner: String,
                      @Path("repo") repo: String): Repository
}

public class SplashModel {
    suspend fun sendNetworkRequestSuspend() = RetrofitHelper.getInstance().createService(SplashInterface::class.java).
            contributors3("square","retrofit")
    
    suspend fun sendNetworkRequest() = RetrofitHelper.getInstance().createService(SplashInterface::class.java).
            contributors2("square","retrofit").await()
}


fun getData() {
        launch(Dispatchers.Main) {
            try {
                getPageView()?.setData(model.sendNetworkRequest())
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
 }