我们一直都在使用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
接口的代理。该方法主要有三个参数分别为:
这里关键点是第三个参数,所有通过调用代理类的代理方法都会在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
是一个简单的模拟打点系统,通过定义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: 每日一算法,由浅入深,欢迎加入一起共勉。
重温Retrofit源码,笑看协程实现
我为何弃用Jetpack的App Startup?
AwesomeGithub组件化探索之旅