盒子
盒子
文章目录
  1. 静态代理
  2. 动态代理
  3. Demo
  4. 项目

动态代理分析与仿Retrofit实践


我们一直都在使用Retroift,都知道它的核心是动态代理。例如在之前的文章重温Retrofit源码,笑看协程实现中也简单提及到动态代理(来填之前挖的坑…)。

咳咳,大家不要关注起因,还是要回归当前的内容。

这次主要是来分析一下动态代理的作用与实现原理。既然都已经分析了原理,最后自然也要动手仿照Retrofit来简单实现一个Demo

通过最后的Demo实现,相信动态代理你也基本没什么问题了。

静态代理

既然说到动态代理,自然少不了静态代理。那么静态代理到底是什么呢?我们还是通过一个简单的场景来了解。

假设有一个Bird接口来代表鸟的一些特性,例如fly飞行特性

1
2
3
interface Bird {
fun fly()
}

现在分别有麻雀、老鹰等动物,因为它们都是鸟类,所以都会实现Bird接口,内部实现自己的fly逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 麻雀
class Sparrow : Bird {
override fun fly() {
println("Sparrow: is fly.")
Thread.sleep(1000)
}
}
// 老鹰
class Eagle : Bird {
override fun fly() {
println("Eagle: is fly.")
Thread.sleep(2000)
}
}

麻雀与老鹰的飞行能力都实现了,现在有个需求:需要分别统计麻雀与老鹰飞行的时长。

你会怎么做呢?相信在我们刚学习编程的时候都会想到的是:这还不简单直接在麻雀与老鹰的fly方法中分别统计就可以了。

如果实现的鸟类种类不多的话,这种实现不会有太大的问题,但是一旦实现的鸟类种类很多,那么这种方法重复做的逻辑将会很多,因为我们要到每一种鸟类的fly方法中都去添加统计时长的逻辑。

所以为了解决这种无意义的重复逻辑,我们可以通过一个ProxyBird来代理实现时长的统计。

1
2
3
4
5
6
7
8
class BirdProxy(private val bird: Bird) : Bird {
override fun fly() {
println("BirdProxy: fly start.")
val start = System.currentTimeMillis() / 1000
bird.fly()
println("BirdProxy: fly end and cost time => ${System.currentTimeMillis() / 1000 - start}s")
}
}

ProxyBird实现了Bird接口,同时接受了外部传进来的实现Bird接口的对象。当调用ProxyBirdfly方法时,间接调用了传进来的对象的fly方法,同时还进行来时长的统计。

1
2
3
4
5
6
7
8
9
10
11
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
ProxyBird(Sparrow()).fly()
println()
ProxyBird(Eagle()).fly()
}
}
}

最后输出如下:

1
2
3
4
5
6
7
ProxyBird: fly start.
Sparrow: is fly.
ProxyBird: fly end and cost time => 1s
ProxyBird: fly start.
Eagle: is fly.
ProxyBird: fly end and cost time => 2s

上面这种模式就是静态代理,可能有许多读者都已经不知觉的使用到了这种方法,只是自己没有意识到这是静态代理。

那它的好处是什么呢?

通过上面的例子,很自然的能够体会到静态代理主要帮我们解决的问题是:

  1. 减少重复逻辑的编写,提供统一的便捷处理入口。
  2. 封装实现细节。

动态代理

既然已经有了静态代理,为什么又要来一个动态代理呢?

任何东西的产生都是有它的必要性的,都是为了解决前者不能解决的问题。

所以动态代理就是来解决静态代理所不能解决的问题,亦或者是它的缺点。

假设我们现在要为Bird新增一种特性:chirp鸟叫。

那么基于前面的静态代理,需要做些什么改变呢?

  1. 修改Bird接口,新增chirp方法。
  2. 分别修改SparrowEagle,为它们新增chirp的具体实现。
  3. 修改ProxyBird,实现chirp代理方法。

1、3还好,尤其是2,一旦实现Bird接口的鸟类种类很多的话,将会非常繁琐,这时就真的是牵一发动全身了。

这还是改动现有的Bird接口,可能你还需要新增另外一种接口,例如Fish鱼,实现有关鱼的特性。

这时又要重新生成一个新的代理ProxyFish来管理有关鱼的代理。

所以从这一点,我们可以发现静态代理的机动性很差,对于那些实现了之后不怎么改变的功能,可以考虑使用它来实现,这也完全符合它的名字中的静态的特性。

那么这种情况动态代理就能够解决吗?别急,能否解决接着往下看。

接着上面,我们为Bird新增chirp方法

1
2
3
4
5
interface Bird {
fun fly()
fun chirp()
}

然后再通过动态代理的方式来实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
val proxy = (Proxy.newProxyInstance(this::class.java.classLoader, arrayOf(Bird::class.java), InvocationHandler { proxy, method, args ->
if (method.name == "fly") {
println("calling fly.")
} else if (method.name == "chirp") {
println("calling chirp.")
}
}) as Bird)
proxy.fly()
proxy.chirp()
}
}
}

输出如下:

1
2
calling fly.
calling chirp.

方式很简单,通过Proxy.newProxyInstance静态方法来创建一个实现Bird接口的代理。该方法主要有三个参数分别为:

  1. ClassLoader: 生成代理类的类类加载器。
  2. interface 接口Class数组: 对应的接口Class。
  3. InvocationHandler: InvocationHandler对象,所有代理方法的回调。

这里关键点是第三个参数,所有通过调用代理类的代理方法都会在InvocationHandler对象中通过它的invoke方法进行回调

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

这就是上面将判断调用具体接口方法的逻辑写在InvocationHandler对象的invoke方法的原因。

那它到底是如何实现的呢?怎么就成了一个代理类呢?我也没看到代理类在哪啊?怎么就所有调用都通过InvocationHandler的呢?

有这些疑问很正常,开始接触动态代理时都会有这些疑问。导致这些疑问的直接原因是我们不能直接看到所谓的代理类。因为动态代理是在运行时生成代理类的,所以不像在编译时期一样能够直接看到源码。

那么下面目标就很明确了,解决看不到源码的问题。

既然是运行时生成的,那么在运行的时候将生成的代理类写到本地目录下不就可以了吗?至于如何写Proxy已经提供了ProxyGenerator。它的generateProxyClass方法能够帮助我们得到生成的代理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
val byte = ProxyGenerator.generateProxyClass("\$Proxy0", arrayOf(Bird::class.java))
FileOutputStream("/Users/{path}/Downloads/\$Proxy0.class").apply {
write(byte)
flush()
close()
}
}
}
}

运行上面的代码就会在Downloads目录下找到$Proxy0.class文件,将其直接拖到编译器中,打开后的具体代码如下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public final class $Proxy0 extends Proxy implements Bird {
private static Method m1;
private static Method m4;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void fly() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void chirp() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m4 = Class.forName("com.daily.algothrim.Bird").getMethod("fly");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.daily.algothrim.Bird").getMethod("chirp");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

首先$Proxy0继承了Proxy同时实现了我们熟悉的Bird接口;然后在它的构造方法中接受了一个var1参数,它的类型是InvocationHandler。继续看方法,实现了类的默认三个方法equalstoStringhashCode,同时也找到了我们需要的flychirp方法。

例如fly方法,调用了

1
super.h.invoke(this, m4, (Object[])null)

这里的h就是之前的var1,即InvocationHandler对象。

到这里迷雾已经揭晓了,调用invoke方法,同时将代理类的自身this、对应的method信息与方法参数传递过去。

所以我们只需要在动态代理的最后一个参数InvocationHandlerinvoke方法中进行处理不同代理方法的相关逻辑。这样做的好处是,不管你如何新增与删除Bird中的接口方法,我都只要调整invoke的处理逻辑即可,将改动的范围缩小到最小化。

这就是动态代理的好处之一(另一个主要的好处自然是减少代理类的书写)。

Android中运用动态代理的典型非Retrofit莫属。由于是一个网络框架,一个App对于网络请求来说接口自然是随着App的迭代不断增加的。对于这种变化频繁的情况,Retrofit使用动态代理为入口,暴露出一个对应的Service接口,而相关的接口请求方法都在Service中进行定义。所以我们每新增一个接口,都不需要做过多的别的修改,相关的网络请求逻辑都封装到动态代理的invoke方法中,当然Retrofit原理是借助添加Annomation注解的方式来解析不同网络请求的方式与相关的参数逻辑。最终再将解析的数据进行封装传递给下层的OKHttp

所以Retrofit的核心就是动态代理与注解的解析。

这篇文章的原理解析部分就完成了,最后既然分析了动态代理与Retrofit的关系,我这里提供了一个Demo来巩固一下动态代理,同时借鉴Retroift的一些思想对一个简易版的打点系统进行上层封装。

Demo

Demo是一个简单的模拟打点系统,通过定义Statistic类来创建动态代理,暴露Service接口,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Statistic private constructor() {
companion object {
@JvmStatic
val instance by lazy { Statistic() }
}
@Suppress("UNCHECKED_CAST")
fun <T> create(service: Class<T>): T {
return Proxy.newProxyInstance(service.classLoader, arrayOf(service)) { proxy, method, args ->
return@newProxyInstance LoadService(method).invoke(args)
} as T
}
}

通过入口传进来的Service接口,从而创建对应的动态代理类,然后将对Service接口中的方法调用的逻辑处理都封装到了LoadServiceinvoke方法中。当然Statistic也借助了注解来解析不同的打点类型事件。

例如,我们需要分别对ButtonText进行点击与展示打点统计。

首先我们可以如下定义对应的Service接口,这里命名为StatisticService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface StatisticService {
@Scan(ProxyActivity.PAGE_NAME)
fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name: String)
@Click(ProxyActivity.PAGE_NAME)
fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
@Scan(ProxyActivity.PAGE_NAME)
fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String)
@Click(ProxyActivity.PAGE_NAME)
fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
}

然后再通过Statistic来获取动态代理的代理类对象

1
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)

有了对应的代理类对象,剩下的就是在对应的位置直接调用。

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
class ProxyActivity : AppCompatActivity() {
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
companion object {
private const val BUTTON = "statistic_button"
private const val TEXT = "statistic_text"
const val PAGE_NAME = "ProxyActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val extraData = getExtraData()
setContentView(extraData.layoutId)
title = extraData.title
// statistic scan
mStatisticService.buttonScan(BUTTON)
mStatisticService.textScan(TEXT)
}
private fun getExtraData(): MainModel =
intent?.extras?.getParcelable(ActivityUtils.EXTRA_DATA)
?: throw NullPointerException("intent or extras is null")
fun onClick(view: View) {
// statistic click
if (view.id == R.id.button) {
mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
} else if (view.id == R.id.text) {
mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
}
}
}

这样一个简单的打点上层逻辑封装就完成了。由于篇幅有限(懒…)内部具体的实现逻辑就不展开了。

相关源码都在android-api-analysis项目中,感兴趣的可以自行查看。

使用前请先把分支切换到feat_proxy_dev

项目

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于Jetpack&DataBindingMVVM;项目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

支持一下
赞赏是一门艺术