️This article has been over 2 years since the last update.
Retrofit2 目前已经越来越主流稳定了,它终于完全抛弃了其它的网络库而是使用了OkHttp3作为依赖,功能也更加插件化了。经常听到动态代理这个词而不知所云,现在知识水平高了,分析一下,以飨(xiǎng)读者。
如果用一个词来概括Retrofit的话,那么“解释器”就是最准确的,可以说解释器是一切计算的根本,从8086中的汇编码MOV 1, eax
让CPU一步步的运行,到Java的字节码,再到Retrofit中的元数据(metadata)解析,都是可以看作解释器Parse的过程。
详见垠神的怎样写一个解释器
在Retrofit中,使用注解(Annotation)作为元数据进行构造阅读友好的请求,接着交给注解处理器作为解释器,对注解进行Parse,在运行时拼装真正的HTTP请求;最后包装了OkHttp,实现了对Rx、线程的adaption。
本文主要是过程(practice),而不是教程(tutorial),所以希望各位下载代码实践,跳出舒适区。
本文基于retrofit:2.0.0-beta3
与jdk1.8.0_05
进行分析
你将学到
- 代理模式的一些简介
- 定义的接口在动态代理使用前与使用后的对比
Retrofit如何完成interface(方法,注解与泛型)到Okhttp请求对象的转换(待填坑)- Retrofit如何使用Okhttp并在回调时进行线程切换,已经另开文章写
准备工具
对字节码进行分析的反编译工具
对字节码保存的字节码保存工具类,用这个可以导出class文件,当然你也可以在jvm添加一些属性参数自动dump
1
2ProxyUtils.saveProxyClass("123.class", "Github", new Class[] { GitHub.class });
Retrofit2的源码,注意它是基于maven的,最好用Idea导入
1
2git clone --depth=1 https://github.com/square/retrofit.git
还不知道Retrofit怎么用?先看看Retrofit解析JSON数据吧
1. 什么是代理
代理是一种设计模式,分为静态代理与动态代理。
没有代理前,调用是这样的:
- 调用者
- 业务细节实现
使用后,它们的调用顺序是这样的:
- 调用者
- 业务逻辑(一般是抽象方法或者是接口)
- 业务细节实现
通过在中间加了一层代理,将业务逻辑与实现细节分层,方便了上层开发者。
1.1. 举例
1.1.1. 办手续
我需要去有关部门办很多手续,但是不想来回跑,于是找到了一家中介。
1 | 我 <-- 次性提供中介要求的几个证件 --> 中介 |
通过代理,我不用了解跑腿的细节,就可以完成业务,这个就是代理的优点,面向业务而忽略细节。当然这个是有代价的(比如需要交跑腿费)。
1.1.2. 上外网
我需要访问国外的网站,但是速度很慢,于是找到了一个服务商。
1 | 我 <-- 一键配置pac --> 中转服务器 |
通过代理,我只用复制一个pac,就可以完成业务,而不用去想怎么与防火墙斗智斗勇,同样这个是需要代价的
1.2. 理论
1.2.1. 静态代理
静态代理通过用户手动
编码或者在编译时
自动生成。
比如说,在 AppCompatActivity 中的AppCompatDelegate
抽象类就是静态代理,而抽象类的实例化是在静态语句块 static{} 区中根据版本号实现的,这样写可以避免 Activity 本身的业务过于冗余,同时将抽象类(也可能是接口)与实现类相分离,这样上层开发者可以专注于业务,而不用管具体的实现。
1 | AppCompatActivity - 委任 - 委任实现 |
在Android中的IPC访问中(比如WMS),如果需要跨进程访问方法,需要使用AIDL通信,当调用者调用远程方法时,调用的是 Stub
类,也就是远程进程的桩,接着这个桩通过 ASHMEM(匿名共享内存)
进行转发,最后方法的实现还是在远程服务中。
1 | 调用者 - Stub接口(由AIDL生成) - 远程服务 |
上面的 stub, delegate 都是静态代理,本身是抽象类的,让调用者实现了面向接口业务编程。
1.2.2. 动态代理
动态代理是java中的字节码生成技术。通过接口生成代理类,并将代理类的实现交给 InvocationHandler
作为具体的实现。
- Retrofit中为了简化复杂的网络请求,通过动态代理的去处理解析与拼装,可以看成为一种基于元编程开发的解释器。
- 希望给某个方法在执行的过程中添加权限控制或者打log,这时我们可以用动态代理生成后在handler中装饰多余的业务,从而保存原有的方法专注于业务本身(即AOP编程),例子在这里。
- 在运行时“欺上瞒下”,可以看作为IOC,比如360的插件化技术的实现,以及Spring的GCLIB的实现
动态代理的过程
动态代理借助java的反射特性,通过易写的接口与注解,帮助用户自动生成实际的对象,接口中方法真正的调用交给InvocationHandler。
Input | interface |
---|---|
↓ | ↓ |
Proxy.newProxyInstance | 对接口进行包装,内部方法由handler委任 |
↓ | ↓ |
Output | interface(with InvocationHandler) |
当用户调用生成接口中的方法时,实际上调用了InvocationHandler中的invoke
方法
现在我们将动态代理前的接口文件,与代理后的运行生成的反编译后的文件进行对比
生成前
官方的示例接口
1 | public interface GitHub { |
官方对接口的调用
1 | Call<List<Contributor>> call = github.contributors("square", "retrofit"); |
代理生成中
它在Retrofit中的create中作为参数传入,通过调用下面方法读取接口信息并在运行时生成字节码并用classloader加载
1 | Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, |
生成后
这是动态代理生成后用工具dump出并反编译的class(下面省略了Object自带的方法,以及大量的异常捕捉)
1 | public final class Github extends Proxy implements SimpleService.GitHub { |
可以看出InvocationHandler是最重要的修饰,Handler中的invoke进行了实际处理,而这些是我们重写过的,至于具体怎么处理的,这些坑后续再补。
动态代理的原理
代理生成的过程使用了sun包中的闭源方法,反编译大致看了一下,先把接口通过native方法ProxyGenerator.generateProxyClass()
根据Class文件的规范拼装生成了byte[]
字节码,这个方法是在jvm(或者说art)的runtime中作为cpp编写的,详见这里。接着用native方法defineClass0()
转换为JVM中的Class结构体,最后通过class加载与连接,最后创建Class,然后通过class.newInstance进行对象的实例化。可以看出动态代理本质上是生成大量样板代码
的过程。相比于静态代理,动态代理可以减少编写代理类(比如XXXImpl)的工作量。
动态代理(反射)是否明显降低了性能?
不会,经过测试如下:
retrofit构造(128ms):
- 构造OkHttp:121ms, 其中javax.ssl构建耗时117ms,这个基本无法避免;缓存文件初始化1ms
- 构造GsonFactory 4ms: 主要是classloader加载的时间
- 其他 3ms
retrofit访问网络前接口的构造(42ms)
- RxJava框架: 12ms
- 动态代理: 1ms
- Gson库: 27ms,主要进行反射操作
- 其他: 2ms
相比于后面的网络/文件请求,完全不是一个数量级。
开发与运行效率不能两全,如果你是强迫症的话,可以让它单例化。