Retrofit 源码阅读笔记(一)
Retrofit
相信每一个 Android
开发者都对它非常熟悉,它使我们调用 Http
请求变得非常的简单(内部实现是 OkHttp
),我们只需要定义一个接口,接口的每一个方法就表示一个 Http
请求,方法的注解,参数的注解和参数他们共同来描述了这一个 Http
请求的 Request
,而方法的返回值描述了如何处理 Http
的 Response
。通过 Retrofit#create()
方法就能够生成一个上面接口的实现类(通过动态代理实现),通过调用这个实现类的方法就能够完成一次 Http
请求,一次网络请求就好像调用了一次我们的本地方法一样。刚开始接触 Retrofit
的我,感觉这个真的是太神奇,工作多年后渐渐地我也知道了它的原理,所以这次文章来基于 Retrofit
的源码来记录一下它的实现方式。
本篇文章是系列文章的第一篇,我阅读的 Retrofit
源码版本是 2.9.0
。
动态代理创建接口实现类
创建我们定义的接口实现类通过 Retrofit#create()
方法传一个接口的 Class
对象。我们来看看它的源码:
上面代码做了两件事,通过 validateServiceInterface()
方法检查 Class
对象是否合法,然后通过动态代理构建一个代理对象(如果对动态代理没有一点了解的同学,建议先去查查相关资料)。
代理对象的每一个方法调用都会触发 InvocationHandler#invoke()
的方法,proxy
是代理的对象,method
是对应的方法的 Method
(常写反射的同学可能很熟悉),args
就是对应方法的参数。invoke()
方法的处理主要做了以下事情:
- 判断是否是
Object
中的方法,如果是直接通过反射的方式调用对应的方法。 - 判断是否是接口中的
default
方法,如果是直接通过Platform#invokeDefaultMethod()
方法去调用。 - 如果第一点和第二点的条件都不满足,通过
loadServiceMethod()
方法加载一个对象,然后调用它的invoke()
方法来执行调用。(这也是我们分析的主要逻辑)
validateServiceInterface()
我们简单介绍一下是如何校验 Class
对象的:
上面主要验证两点:
-
Class
对象必须是接口。 -
Class
对象和其对应的父类接口都不能有泛型参数。
后续还通过 validateEagerly
参数来判断是否要提前加载对应的请求 Method
,默认是不提前加载,只有在方法调用时才会去判断加载。
loadServiceMethod()
加载方法的实现是通过 loadServiceMethod()
,看看代码实现:
请求方法解析完成后使用 ServiceMethod
类封装,首先从本地缓存中去获取 ServiceMethod
对象,如果没有再通过 ServiceMethod#parseAnnotaions()
方法去创建,然后存放在缓存中,下次再使用就不需要再创建。
这里有一个有意思的问题就是这里的锁用的是 serviceMethodCache
,用的锁范围稍微大了一点点,也就相当于所有的 Service
创建都会竞争这个锁,这里有相关的 Issue,这个锁的竞争还造成了 ANR
。有一个大佬把上面的锁的对象修改成了 Service
的 Class
对象,这个问题就得到了很大的缓解。
上面的代码主要做了以下事情:
- 通过
RequestFactory.parseAnnotations()
方法来解析方法的注解和参数的注解来构建Http
的请求。 - 校验方法返回值的类型,返回类型中不能够有不确定的泛型,也不能够是返回空。
- 通过
HttpServiceMethod.parseAnnotations()
方法来构建Http
的请求任务。
RequestFactory.parseAnnotations()
和 HttpServiceMethod.parseAnnotations()
这两个方法可以说是核心代码中的核心,后续的分析也都从它们两个开始。
方法注解解析
我们接着看前面一节提到的 RequestFactory.parseAnnotations()
方法:
朴实无华的代码,直接创建一个 Builder()
对象,然后调用其 build()
方法,我们看看 它的构造函数和 build()
方法的源码实现:
构造函数中首先获取到方法的注解(可以有多个注解,所以是一个数组);获取方法中参数的类型;获取方法参数中的注解(因为有多个参数,一个参数又可以有多个注解,所以是一个二维数组)。
build()
方法中主要做了两件事,通过 parseMethodAnnotation()
来解析方法的注解;通过 parseParameter()
来解析参数和对应的注解,解析成功后对应的处理方式用 ParameterHandler
类来封装。
在 build()
函数中还判断了各种请求参数是否正确,有以下几点:
- 如果不知道
Http
请求的类型(就是GET
,POST
啥的没有明确)报错。 - 如果
Multipart
和FormEncoded
没有Body
报错。 - 如果没有获取到
Url
和 请求的Path
,报错 (也就是Url
和 请求的Path
取其一即可) 。 - 不允许有
Body
的请求,但是有Body
,报错(比如GET
请求就不允许有Body
)。 -
FormEncoded
请求没有获取到Field
, 报错。 -
Multipart
请求,没有获取到Part
Body
,报错。
我们看看 parseMethodAnnotation()
方法的源码:
如果是请求方法类的注解通过 parseHttpMethodAndPath()
方法去解析它的请求方法和解析请求的相对路径,请求方法类的注解包括 @DELETE
、@GET
、 @HEAD
、 @PATCH
、 @POST
、 @PUT
、 @OPTIONS
、 和 @HTTP
等等。@Headers
注解表示请求的 Header
,通过 parseHeaders()
方法完成解析。@Multipart
标记请求 Body
为 Multipart
,它和 FormUrlEncoded
冲突。@FormUrlEncoded
标记请求 Body
为 Form
表单,它和 Multipart
冲突。
我们看看 parseHttpMethodAndPath()
方法的实现:
这里主要做了以下的事情:
- 如果已经设置过了
HttpMethod
,报错。 - 如果
Query
参数后有参数化的Path
设置,报错。 - 记录相对路径
Path
和解析Path
参数化的名字。
可能你有点忘了什么是参数化的 Path
,我这里简单介绍一下,parseHttpMethodAndPath()
中只能处理相对路径 Path
,比如 /a/b/c
就是一个相对路径,参数化就是比如我定义的 Path
是这样的 /a/{name}/c
,其中的 {name}
就是一个占位符,可以通过参数中的注解 @Path
来替换它(后续的文章中会看到它),比如我的参数 name
是 tans
,那么请求是的 Path
就是 /a/tans/c
。
上面的 Path
是可以为空的,如果为空就需要参数的注解中有 @Url
(后续的文章中会看到它),它是来表示一个绝对路径的,比如 https://www.tans.com/a/b/c
。相对路径和绝对路径只能有一个存在。
我们再看看 parseHeaders()
方法的实现:
Http
协议的 Header
的 Name
和 Value
都是以 :
分割开,上面的解析代码也是非常的简单,就不多说了。
最后
本篇文章中介绍了 Retrofit
的动态代理实现、方法注解解析等等内容,后续的文章还会继续介绍方法参数和参数注解的解析,请求任务的解析等等逻辑。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。