Retrofit2源码分析[动态代理]
2016-01-20 / modified at 2022-04-04 / 2.3k words / 9 mins
️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-beta3jdk1.8.0_05进行分析


你将学到

  1. 代理模式的一些简介
  2. 定义的接口在动态代理使用前与使用后的对比
  3. Retrofit如何完成interface(方法,注解与泛型)到Okhttp请求对象的转换(待填坑)
  4. Retrofit如何使用Okhttp并在回调时进行线程切换,已经另开文章写

准备工具

  1. 对字节码进行分析的反编译工具

  2. 对字节码保存的字节码保存工具类,用这个可以导出class文件,当然你也可以在jvm添加一些属性参数自动dump

    1
    2
    ProxyUtils.saveProxyClass("123.class", "Github", new Class[] { GitHub.class });

  3. Retrofit2的源码,注意它是基于maven的,最好用Idea导入

    1
    2
    git clone --depth=1 https://github.com/square/retrofit.git

  4. 还不知道Retrofit怎么用?先看看Retrofit解析JSON数据

1. 什么是代理

代理是一种设计模式,分为静态代理与动态代理。

没有代理前,调用是这样的:

  • 调用者
  • 业务细节实现

使用后,它们的调用顺序是这样的:

  • 调用者
  • 业务逻辑(一般是抽象方法或者是接口)
  • 业务细节实现

通过在中间加了一层代理,将业务逻辑与实现细节分层,方便了上层开发者。

1.1. 举例

1.1.1. 办手续

我需要去有关部门办很多手续,但是不想来回跑,于是找到了一家中介。

1
2
我 <-- 次性提供中介要求的几个证件 --> 中介
中介 <-- 进行繁琐的填表跑腿工作 --> 有关部门

通过代理,我不用了解跑腿的细节,就可以完成业务,这个就是代理的优点,面向业务而忽略细节。当然这个是有代价的(比如需要交跑腿费)。

1.1.2. 上外网

我需要访问国外的网站,但是速度很慢,于是找到了一个服务商。

1
2
我 <-- 一键配置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 作为具体的实现。

  1. Retrofit中为了简化复杂的网络请求,通过动态代理的去处理解析与拼装,可以看成为一种基于元编程开发的解释器。
  2. 希望给某个方法在执行的过程中添加权限控制或者打log,这时我们可以用动态代理生成后在handler中装饰多余的业务,从而保存原有的方法专注于业务本身(即AOP编程),例子在这里
  3. 在运行时“欺上瞒下”,可以看作为IOC,比如360的插件化技术的实现,以及Spring的GCLIB的实现

动态代理的过程

动态代理借助java的反射特性,通过易写的接口与注解,帮助用户自动生成实际的对象,接口中方法真正的调用交给InvocationHandler。

Inputinterface
Proxy.newProxyInstance对接口进行包装,内部方法由handler委任
Outputinterface(with InvocationHandler)

当用户调用生成接口中的方法时,实际上调用了InvocationHandler中的invoke方法

现在我们将动态代理前的接口文件,与代理后的运行生成的反编译后的文件进行对比

生成前

官方的示例接口

1
2
3
4
public interface GitHub {
@GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> contributors(
@Path("owner") String owner, @Path("repo") String repo);
}

官方对接口的调用

1
Call<List<Contributor>> call = github.contributors("square", "retrofit");

代理生成中

它在Retrofit中的create中作为参数传入,通过调用下面方法读取接口信息并在运行时生成字节码并用classloader加载

1
2
3
4
5
6
7
8
9
10
11
Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();

@Override public Object invoke(Object proxy, Method method, Object... args)
.....
//开始处理注解并拼装成OkHttp的call
return loadMethodHandler(method).invoke(args);
}
});
}

生成后

这是动态代理生成后用工具dump出并反编译的class(下面省略了Object自带的方法,以及大量的异常捕捉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final class Github extends Proxy implements SimpleService.GitHub {
private static Method m0;//自带的hashCode方法
private static Method m1;//自带的equals方法
private static Method m2;//自带的toString
private static Method m3;//真正的接口中的方法

static {
try {
//省略Object自带方法的初始化
m0,m1,m2 = ...
//接口中真正的方法
m3 = Class.forName("com.example.retrofit.SimpleService$GitHub")
.getMethod("contributors",
new Class[] { Class.forName("java.lang.String"), Class.forName("java.lang.String") });
} catch (Throwable e) {
throw new NoSuchMethodError(e.getMessage());
} catch (Throwable e2) {
throw new NoClassDefFoundError(e2.getMessage());
}
}

public Github(InvocationHandler invocationHandler) {
super(invocationHandler);
}

//这里的参数实际上就是用户输入的"square", "retrofit"
public final Call contributors(String str, String str2) {
RuntimeException e;
try {
//this.h是InvocationHandler,也就是上文重写的Handler
//可以看出实际调用的是handler的`invoke`方法
return (Call) this.h.invoke(this, m3, new Object[] { str, str2 });
} catch (Throwable e) {
throw e;
}
}

....Object自带方法的代理...
}

可以看出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

相比于后面的网络/文件请求,完全不是一个数量级。

开发与运行效率不能两全,如果你是强迫症的话,可以让它单例化。

Refference

  1. https://www.ibm.com/developerworks/cn/java/j-lo-proxy1/
  2. http://a.codekk.com/detail/Android/Caij/公共技术点之 Java 动态代理
  3. http://docs.oracle.com/javase/tutorial/reflect/
  4. http://tutorials.jenkov.com/java-reflection/generics.html
  5. http://www.ibm.com/developerworks/cn/java/j-jtp08305.html