38

动态代理分析与仿Retrofit实践

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw%3D%3D&%3Bmid=2247484600&%3Bidx=1&%3Bsn=d0b0546d81aa28ba82d65f3451a01595
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们一直都在使用 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. Sparrow
    Eagle
    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: https://github.com/idisfkj/android-startup

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

AwesomeGithub : https://github.com/idisfkj/AwesomeGithub

基于 Github 客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。 使用 Kotlin 语言进行开发,项目架构是基于 Jetpack&DataBinding MVVM 项目中使用了 Arouter Retrofit Coroutine Glide Dagger Hilt 等流行开源技术。

flutter_github: https://github.com/idisfkj/flutter_github

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

android-api-analysis: https://github.com/idisfkj/android-api-analysis

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

daily_algorithm https://github.com/idisfkj/daily_algorithm

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

推荐阅读

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

我为何弃用Jetpack的App Startup?

AwesomeGithub组件化探索之旅

原创不易,求赞支持

yMFfa2V.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK