美文网首页Android补给站
动态代理分析与仿Retrofit实践

动态代理分析与仿Retrofit实践

作者: 午后一小憩 | 来源:发表于2020-10-29 18:32 被阅读0次

    我们一直都在使用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接口的对象。当调用ProxyBirdfly方法时,间接调用了传进来的对象的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
    

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

    那它的好处是什么呢?

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

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

    动态代理

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

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

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

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

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

    1. 修改Bird接口,新增chirp方法。
    2. 分别修改SparrowEagle,为它们新增chirp的具体实现。
    3. 修改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接口的代理。该方法主要有三个参数分别为:

    1. ClassLoader: 生成代理类的类类加载器。
    2. interface 接口Class数组: 对应的接口Class。
    3. 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。继续看方法,实现了类的默认三个方法equalstoStringhashCode,同时也找到了我们需要的flychirp方法。

    例如fly方法,调用了

    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接口,具体如下:

    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

    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&DataBindingMVVM;项目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行开源技术。

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

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

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

    推荐阅读

    重温Retrofit源码,笑看协程实现

    我为何弃用Jetpack的App Startup?

    AwesomeGithub组件化探索之旅

    相关文章

      网友评论

        本文标题:动态代理分析与仿Retrofit实践

        本文链接:https://www.haomeiwen.com/subject/fyzhvktx.html