动态代理分析与仿Retrofit实践
我们一直都在使用Retroift
,都知道它的核心是动态代理。例如在之前的文章重温Retrofit源码,笑看协程实现中也简单提及到动态代理(来填之前挖的坑...)。
咳咳,大家不要关注起因,还是要回归当前的内容。
这次主要是来分析一下动态代理的作用与实现原理。既然都已经分析了原理,最后自然也要动手仿照Retrofit
来简单实现一个Demo
。
通过最后的Demo
实现,相信动态代理你也基本没什么问题了。
静态代理
既然说到动态代理,自然少不了静态代理。那么静态代理到底是什么呢?我们还是通过一个简单的场景来了解。
假设有一个Bird
接口来代表鸟的一些特性,例如fly
飞行特性
interface Bird {
fun fly()
}
现在分别有麻雀、老鹰等动物,因为它们都是鸟类,所以都会实现Bird
接口,内部实现自己的fly
逻辑。
// 麻雀
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
来代理实现时长的统计。
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
接口的对象。当调用ProxyBird
的fly
方法时,间接调用了传进来的对象的fly
方法,同时还进行来时长的统计。
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
ProxyBird(Sparrow()).fly()
println()
ProxyBird(Eagle()).fly()
}
}
}
最后输出如下:
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
上面这种模式就是静态代理,可能有许多读者都已经不知觉的使用到了这种方法,只是自己没有意识到这是静态代理。
那它的好处是什么呢?
通过上面的例子,很自然的能够体会到静态代理主要帮我们解决的问题是:
- 减少重复逻辑的编写,提供统一的便捷处理入口。
- 封装实现细节。
动态代理
既然已经有了静态代理,为什么又要来一个动态代理呢?
任何东西的产生都是有它的必要性的,都是为了解决前者不能解决的问题。
所以动态代理就是来解决静态代理所不能解决的问题,亦或者是它的缺点。
假设我们现在要为Bird
新增一种特性:chirp
鸟叫。
那么基于前面的静态代理,需要做些什么改变呢?
- 修改
Bird
接口,新增chirp
方法。 - 分别修改
Sparrow
与Eagle
,为它们新增chirp
的具体实现。 - 修改
ProxyBird
,实现chirp
代理方法。
1、3还好,尤其是2,一旦实现Bird
接口的鸟类种类很多的话,将会非常繁琐,这时就真的是牵一发动全身了。
这还是改动现有的Bird
接口,可能你还需要新增另外一种接口,例如Fish
鱼,实现有关鱼的特性。
这时又要重新生成一个新的代理ProxyFish
来管理有关鱼的代理。
所以从这一点,我们可以发现静态代理的机动性很差,对于那些实现了之后不怎么改变的功能,可以考虑使用它来实现,这也完全符合它的名字中的静态的特性。
那么这种情况动态代理就能够解决吗?别急,能否解决接着往下看。
接着上面,我们为Bird
新增chirp
方法
interface Bird {
fun fly()
fun chirp()
}
然后再通过动态代理的方式来实现这个接口
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()
}
}
}
输出如下:
calling fly.
calling chirp.
方式很简单,通过Proxy.newProxyInstance
静态方法来创建一个实现Bird
接口的代理。该方法主要有三个参数分别为:
- ClassLoader: 生成代理类的类类加载器。
- interface 接口Class数组: 对应的接口Class。
- InvocationHandler: InvocationHandler对象,所有代理方法的回调。
这里关键点是第三个参数,所有通过调用代理类的代理方法都会在InvocationHandler
对象中通过它的invoke
方法进行回调
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
这就是上面将判断调用具体接口方法的逻辑写在InvocationHandler
对象的invoke
方法的原因。
那它到底是如何实现的呢?怎么就成了一个代理类呢?我也没看到代理类在哪啊?怎么就所有调用都通过InvocationHandler
的呢?
有这些疑问很正常,开始接触动态代理时都会有这些疑问。导致这些疑问的直接原因是我们不能直接看到所谓的代理类。因为动态代理是在运行时生成代理类的,所以不像在编译时期一样能够直接看到源码。
那么下面目标就很明确了,解决看不到源码的问题。
既然是运行时生成的,那么在运行的时候将生成的代理类写到本地目录下不就可以了吗?至于如何写Proxy
已经提供了ProxyGenerator
。它的generateProxyClass
方法能够帮助我们得到生成的代理类。
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
文件,将其直接拖到编译器中,打开后的具体代码如下:
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
。继续看方法,实现了类的默认三个方法equals
、toString
与hashCode
,同时也找到了我们需要的fly
与chirp
方法。
例如fly
方法,调用了
super.h.invoke(this, m4, (Object[])null)
这里的h
就是之前的var1
,即InvocationHandler
对象。
到这里迷雾已经揭晓了,调用invoke
方法,同时将代理类的自身this
、对应的method
信息与方法参数传递过去。
所以我们只需要在动态代理的最后一个参数InvocationHandler
的invoke
方法中进行处理不同代理方法的相关逻辑。这样做的好处是,不管你如何新增与删除Bird
中的接口方法,我都只要调整invoke
的处理逻辑即可,将改动的范围缩小到最小化。
这就是动态代理的好处之一(另一个主要的好处自然是减少代理类的书写)。
在Android
中运用动态代理的典型非Retrofit
莫属。由于是一个网络框架,一个App
对于网络请求来说接口自然是随着App
的迭代不断增加的。对于这种变化频繁的情况,Retrofit
使用动态代理为入口,暴露出一个对应的Service
接口,而相关的接口请求方法都在Service
中进行定义。所以我们每新增一个接口,都不需要做过多的别的修改,相关的网络请求逻辑都封装到动态代理的invoke
方法中,当然Retrofit
原理是借助添加Annomation
注解的方式来解析不同网络请求的方式与相关的参数逻辑。最终再将解析的数据进行封装传递给下层的OKHttp
。
所以Retrofit
的核心就是动态代理与注解的解析。
这篇文章的原理解析部分就完成了,最后既然分析了动态代理与Retrofit
的关系,我这里提供了一个Demo
来巩固一下动态代理,同时借鉴Retroift
的一些思想对一个简易版的打点系统进行上层封装。
Demo
Demo
是一个简单的模拟打点系统,通过定义Statistic
类来创建动态代理,暴露Service
接口,具体如下:
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
接口中的方法调用的逻辑处理都封装到了LoadService
的invoke
方法中。当然Statistic
也借助了注解来解析不同的打点类型事件。
例如,我们需要分别对Button
与Text
进行点击与展示打点统计。
首先我们可以如下定义对应的Service
接口,这里命名为StatisticService
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
来获取动态代理的代理类对象
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)
有了对应的代理类对象,剩下的就是在对应的位置直接调用。
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&DataBinding
的MVVM
;项目中使用了Arouter
、Retrofit
、Coroutine
、Glide
、Dagger
与Hilt
等流行开源技术。
flutter_github: 基于Flutter
的跨平台版本Github
客户端,与AwesomeGithub
相对应。
android-api-analysis: 结合详细的Demo
来全面解析Android
相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。
daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。