协程系列(二) Coroutines and Channels

656 阅读4分钟

本文基于协程官方文档讲解,具体可查看here

一、分析问题

定义接口

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    fun getOrgReposCall(@Path("org") org: String): Call<List<Repo>>

    @GET("repos/{owner}/{repo}/contributors?per_page=100")
    fun getRepoContributorsCall(@Path("owner") owner: String, @Path("repo") repo: String): Call<List<User>>
}

getOrgReposCall先调用获取指定机构org的所有仓库Repo,然后调用getRepoContributorsCall获取每个仓库的贡献者Contributors。两接口存在先后调用关系,这个接口定义就是简单的Retrofit的Call调用。最后拿到结果后更新ui(updateResults)。
问题:怎么实现高效的嵌套调用以上两接口?

二、解决问题

2.1、阻塞UI调用的方式(不建议)

fun loadContributorsBlocking(service: GitHubService, req: RequestData) : List<User> {
    //获取org所有的仓库列表
    val repos = service
        .getOrgReposCall(req.org)
        .execute() // Executes request and blocks the current thread
        .body() ?: listOf()
    //获取所有的仓库的贡献者
    return repos.flatMap { repo ->
        service
            .getRepoContributorsCall(req.org, repo.name)
            .execute() // okhttp的同步调用
            .body()?: listOf()
    }
}

获取所有的List<User>,使用flatMap非常方便。

//发起请求,更新Ui
val users = loadContributorsBlocking(service, req)
updateResults(users, startTime)

注意接口调用用的okhttp的execute()同步方法,阻塞主线程,当然不建议啦。
调用接口描述如下: image.png

2.2、异步线程方式实现

将接口请求放到子线程去,还是串行执行的。

thread {
    val users = loadContributorsBlocking(service, req)
    handler.post {
        updateResults(users, startTime)
    }
}

接口调用描述如下: image.png

2.3、callback方式实现

2.3.1、使用原子类AtomicInteger和支持并发的集合Collections.synchronizedList实现

fun loadContributorsCallback(service: GitHubService, req: RequestData) {
    //获取用户所有的仓库列表
    val repos = service
        .getOrgReposCall(req.org)
        .execute() // 同步请求
        .body() ?: listOf()
 val allUsers = Collections.synchronizedList(mutableListOf<User>())
    val numberOfProcessed = AtomicInteger() 
    for(repo in repos){
        //多个请求异步发起
        service.getRepoContributorsCall(req.org,repo.name).enqueue(object: Callback<List<User>> {
            override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
                response.body()?.let{
                    allUsers += it
                }
                //当所有的请求都走完了,就可以更新ui了
                if(numberOfProcessed.incrementAndGet() ==repos.size){
                    updateResults(allUsers,startTime)
                }
            }
            override fun onFailure(call: Call<List<User>>, t: Throwable) {
                //no care 不要纠结请求失败的情况
            }
        })
    }
}

2.3.2、使用CountDownLatch和支持并发集合Collections.synchronizedList实现

fun loadContributorsCallback(service: GitHubService, req: RequestData) {
    //获取用户所有的仓库列表
    val repos = service
        .getOrgReposCall(req.org)
        .execute() // Executes request and blocks the current thread
        .body() ?: listOf()
   val allUsers = Collections.synchronizedList(mutableListOf<User>())
    val countDownLatch = CountDownLatch(repos.size) 
    for(repo in repos){
        //多个请求异步发起
        service.getRepoContributorsCall(req.org,repo.name).enqueue(object: Callback<List<User>> {
            override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) {
                response.body()?.let{
                    allUsers += it
                }
               countDownLatch.countDown(); 
            }
            override fun onFailure(call: Call<List<User>>, t: Throwable) {
                //no care 不要纠结请求失败的情况
            }
        })
    }
   countDownLatch.await() 
    updateResults(allUsers,startTime)
}

以上两种方式获取仓库的贡献者都是enqueue异步调用,走的是okhttp的线程池
接口调用描述如下: image.png

2.3、使用挂起函数实现

接口定义如下:

interface GitHubService {
    @GET("orgs/{org}/repos?per_page=100")
    suspend fun getOrgRepos(@Path("org") org: String): Response<List<Repo>>

    @GET("repos/{owner}/{repo}/contributors?per_page=100")
    suspend fun getRepoContributors(@Path("owner") owner: String, @Path("repo") repo: String):Response<List<User>>
}
fun request(view: android.view.View) {
        val service= createGitHubService("your github username","your personal token")
        GlobalScope.launch {
           flow<List<User>> {
              emit(loadContributorsSuspend(service,
                  RequestData("your github username","your personal token","kotlin")
              ))
           }.flowOn(Dispatchers.IO)
            .collectLatest{it->
                updateResults(it)
           }
        }
    }
    suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
        val repos = service
            .getOrgRepos(req.org)
            .body()?: listOf()
        return repos.flatMap { repo ->
            service.getRepoContributors(req.org, repo.name)
                .body()?: listOf()
        }
    }

}

👆发起请求用了GlobalScope,在实际场景中尽量不要用,GlobalScope是毒瘤,可以viewModelScope,这里只是为了方便。 flowOn指定flow上游(这里emit)所处的线程,GlobalScope.launch所调度的线程就是发起接口请求所在的线程,也是flow的collect下游消费所处的线程。
前面讲retrofit支持协程调用时,发起请求,其实走的是okhttp的异步调用

suspend fun <T : Any> Call<T>.await(): T {
 //支持可取消的协程
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      //取消接口请求
      cancel()
    }
    //走okhttp的异步接口请求
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
            val invocation = call.request().tag(Invocation::class.java)!!
            val method = invocation.method()
            val e = KotlinNullPointerException("Response from " +method.declaringClass.name + '.' +method.name +" was null but response body type was declared as non-null")
            continuation.resumeWithException(e)
          } else {
            continuation.resume(body)
          }
        } else {
          continuation.resumeWithException(HttpException(response))
        }
      }
      override fun onFailure(call: Call<T>, t: Throwable) {
        continuation.resumeWithException(t)
      }
    })
  }
}

这里的挂起函数方式实现的接口调用还是串行执行的
接口调用描述如下: image.png

2.4、 使用async+await实现并发请求

fun request(view: android.view.View) {
        val service= createGitHubService("your github username","your personal token")
        GlobalScope.launch {
           flow<List<User>> {
              emit(loadContributorsSuspendConcurrent(service,
                  RequestData("your github username","your personal token","kotlin")
              ))
           }.flowOn(Dispatchers.IO)
            .collectLatest{it->
                updateResults(it)
           }
     }
}
suspend fun loadContributorsSuspendConcurrent(service: GitHubService, req: RequestData): List<User>
                            = coroutineScope {
    val repos = service.getOrgRepos(req.org).body()?: listOf()
    val deferreds:List<Deferred<List<User>>> = repos.map{ repo->
        async {
            service.getRepoContributors(req.org,repo.name).body()?: listOf()
        }
    }
    deferreds.awaitAll().flatten()
}

使用async和awaitAll很经典。
接口调用描述如下: image.png

三、问题进阶

3.1、Q:如果想要获取到每个贡献者接口就回调一下,更新UI,怎么实现?

3.2、解决方法一串行实现:

 GlobalScope.launch(Dispatchers.Default) {
            loadContributorsProgress(service,
            RequestData("your github username","your personal token","kotlin")){list,complete->
                   withContext(Dispatchers.Main){
                       log("打印用户信息"+"${list} 更新Ui")
                   }
            }
    }
}
suspend fun loadContributorsProgress(service: GitHubService, req: RequestData,updateResult:suspend(List<User>,complete:Boolean)->Unit){
    val repos = service
        .getOrgRepos(req.org)
        .body()?: listOf()
    var allUsers=emptyList<User>()
    for((index,repo) in repos.withIndex()){
        log("发起请求")
        val users=service.getRepoContributors(req.org, repo.name).body()?: listOf()
        allUsers+=users;
        updateResult(allUsers,index==repos.lastIndex)
    }
}

updateResult这里传入了一个函数,也挺巧妙的。

3.2、解决方法二:使用channel来实现

channel的最佳使用场景就是协程间通信

//发起请求
GlobalScope.launch(Dispatchers.Default) {
        loadContributorsChannels(
            service,
             RequestData("your github username","your personal token","kotlin")
        ) { list, complete ->
            withContext(Dispatchers.Main) {
                log("打印用户信息" + "${list}")
            }
        }
    }
}
suspend fun loadContributorsChannels(service: GitHubService,req: RequestData,
    updateResult: suspend (List<User>, complete: Boolean) -> Unit) = coroutineScope {
        val repos = service
            .getOrgRepos(req.org)
            .body() ?: listOf()
        val channel = Channel<List<User>>()
        for (repo in repos) {
            launch { //没有使用新的调度器,是因为实际执行接口请求走的是okhttp的异步调用
                val users = service.getRepoContributors(req.org, repo.name).body() ?: listOf()
                channel.send(users)
            }
        }
        var allUser = emptyList<User>()
        repeat(repos.size) {
            val users = channel.receive()
            allUser += users
            updateResult(allUser, it == repos.lastIndex)
        }
    }
}

它的 launch 没有使用新的调度器,是因为实际执行接口请求走的是okhttp的异步调用,大家在写代码的时候,尽量减少线程切换。