本文基于协程官方文档讲解,具体可查看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()同步方法,阻塞主线程,当然不建议啦。
调用接口描述如下:
2.2、异步线程方式实现
将接口请求放到子线程去,还是串行执行的。
thread {
val users = loadContributorsBlocking(service, req)
handler.post {
updateResults(users, startTime)
}
}
接口调用描述如下:
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的线程池
接口调用描述如下:
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)
}
})
}
}
这里的挂起函数方式实现的接口调用还是串行执行的
接口调用描述如下:
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很经典。
接口调用描述如下:
三、问题进阶
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的异步调用,大家在写代码的时候,尽量减少线程切换。