乐趣区

关于android:动态代理分析与仿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 接口的对象。当调用 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 组件化探索之旅

退出移动版