Retrofit解密:接口请求是如何适配suspend协程?

本篇文章主要是带领大家研究retrofit源码一个知识点:如何适配suspend关键字并开启协程执行网络请求的。

最初的retrofit请求

我们先看下原来如何通过retrofit发起一个网络请求的,这里我们直接以官网的例子举例:

动态代理创建请求服务

interface GitHubService {
    //创建get请求方法
    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String?): Call<Response>
}

//动态代理创建GitHubService
fun createService(): GitHubService {
    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .build()

    return retrofit.create(GitHubService::class.java)
}
  • retrofit.create底层是通过动态代理创建的GitHubService的一个子实现类;
  • 创建的这个GitHubService一般作为单例进行使用,这里只是简单举例没有实现单例;

发起网络请求

fun main() {
    //异步执行网络请求
    createService().listRepos("").enqueue(object : Callback<Response> {
        override fun onResponse(call: Call<Response>, response: retrofit2.Response<Response>) {
            //主线程网络请求成功回调
        }

        override fun onFailure(call: Call<Response>, t: Throwable) {
            //主线程网络请求失败回调
        }
    })
}

这种调用enqueue()异步方法并执行callback的方式是不是感觉很麻烦,如果有下一个请求依赖上一个请求的执行结果,那就将会形成回调地狱这种可怕场景。

而协程suspend本就有着以同步代码编写执行异步操作的能力,所以天然是解决回调地狱好帮手。接下来我们看下如何使用协程suspend。

借助suspend发起网络请求

suspend声明接口方法

interface GitHubService {
    @GET("users/{user}/repos")
    suspend fun listRepos(@Path("user") user: String?): Response<String>
}

可以看到就是在listRepos方法声明前加了个suspend关键字就完了。

创建协程执行网络请求

fun main() {
    //1.创建协程作用域,需要保证协程的调度器是分发到主线程执行
    val scope = MainScope()
    scope.launch(CoroutineExceptionHandler { _, _ ->
        //2.捕捉请求异常
    }) {
        //3.异步执行网络请求
        val result = createService().listRepos("")
        
        val content = result.body()?
    }
}

1.首先创建一个协程作用域,需要保证协程调度器类型为Dispatchers.Main,这样整个协程的代码块都会默认在主线程中执行,我们就可以直接在里面执行UI相关操作

2.创建一个CoroutineExceptionHandler捕捉协程执行过程中出现的异常,这个捕捉异常的粒度比较大,是捕捉整个协程块的异常,可以考虑使用try-catch专门捕获网络请求执行的异常:

//异步执行网络请求
try {
    val result = createService().listRepos("")
} catch (e: Exception) {
    //可以考虑执行重连等逻辑或者释放资源
}

直接调用listRepos()方法即可,不需要传入任何回调,并直接返回方法结果。这样我们就实现了以同步的代码实现了异步网络请求。

接下来我们就看下如何retrofit源码是如何实现这一效果的。

retrofit如何适配suspend

直接定位到HttpServiceMethod.parseAnnotations()方法:

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
    Retrofit retrofit, Method method, RequestFactory requestFactory) {
  //1.判断是否为suspend挂起方法
  boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;

  //省略一堆和当前分析主题不想关的代码

  if (!isKotlinSuspendFunction) {
    return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
  } else if (continuationWantsResponse) {
    //挂起执行
    return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>();
  } else {
    //挂起执行
    return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>();
  }
}

1.判断是否为suspend挂起方法

看下requestFactory.isKotlinSuspendFunction赋值的地方,经过一番查找(省略…),最终方法在RequestFactoryparseParameter间接赋值:

private @Nullable ParameterHandler<?> parseParameter() {
//...
    //1.是否是方法最后一个参数
    if (allowContinuation) {
      try {
        if (Utils.getRawType(parameterType) == Continuation.class) {
          //2.标识为suspend挂起方法
          isKotlinSuspendFunction = true;
          return null;
        }
      } catch (NoClassDefFoundError ignored) {
      }
    }
}

如果一个方法被声明为suspend,该方法翻译成java代码就会给该方法添加一个Continuation类型的参数,并且放到方法参数的最后一个位置,比如:

private suspend fun test66(name: String) {  
}

会被翻译成:

private final Object test66(String name, Continuation $completion) {
   return Unit.INSTANCE;
}

所以上面的代码就可以判断出请求的接口方法是否被suspend声明,是isKotlinSuspendFunction将会被置为true。

2.挂起则创建SuspendForResponse或SuspendForBody

这个地方我们以SuspendForBody进行分析,最终会执行到其adapt()方法:

@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
  call = callAdapter.adapt(call);
  //1.获取参数
  Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
  
  try {
    return isNullable
        ? KotlinExtensions.awaitNullable(call, continuation)
        //2.调用真正的挂起方法
        : KotlinExtensions.await(call, continuation);
  } catch (Exception e) {
    return KotlinExtensions.suspendAndThrow(e, continuation);
  }
}
  1. 获取调用的suspend声明的接口方法中获取最后一个Continuation类型参数

  2. 调用await方法,由于这是一个kotlin定义的接收者为Call的挂起方法,如果在java中调用,首先第一个参数要传入接收者,也就是call,其实await()是一个挂起方法,翻译成java还会增加一个Continuation类型参数,所以调用await()还要传入第一步获取的Continuation类型参数。

3.核心调用await()方法探究

await()就是retrofit适配suspend实现同步代码写异步请求的关键,也是消除回调地狱的关键:

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
              //关键
            continuation.resumeWithException(KotlinNullPointerException())
          } else {
              //关键
            continuation.resume(body)
          }
        } else {
            //关键
          continuation.resumeWithException(HttpException(response))
        }
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
          //关键
        continuation.resumeWithException(t)
      }
    })
  }
}

使用到了协程的一个非常关键的方法suspendCancellableCoroutine{},该方法就是用来捕获传入的Continuation并决定什么恢复挂起的协程执行的,比如官方的delay()方法也是借助该方法实现的。

所以当我们执行调用enqueue()方法时在网络请求没有响应(成功或失败)前,协程一直处于挂起的状态,之后收到网络响应后,才会调用resume()resumeWithException()恢复挂起协程的执行,这样我们就实现了同步代码实现异步请求的操作,而不需要任何的callback嵌套地狱。

总结

本篇文章详细分析retrofit如何适配suspend协程的,并且不用编写任何的callback回调,直接以同步代码编写实现异步请求的操作。

作者:长安皈故里
链接:https://juejin.cn/post/7127799209918464013

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值