【Android设计模式】从Retrofit看懂动态代理模式

会写代码是门槛,写好代码是本事

 

前言

平时写代码的时候可能为了完成某一个任务而只是应付性地编码,然后写完理直气壮地来一句"又不是不能用!",但如果要把编码当作一项艺术来打造,那就需要结合我们的设计模式了。设计模式可以运用在诸多方面,让我们的代码更加优雅。设计模式在Android中也是无处不在,动态代理、建造者、工厂、适配器....等等等等,可见它对我们的重要性。最近在看Retrofit的源码,刚好看到了动态代理,想着总结下这个模式。

 

什么是代理模式?

代理类在程序运行前不存在、运行时由程序动态生成的代理方式称为动态代理。

乍这么一看,好像还是说不明白,代理模式分为两种,静态代理和动态代理,静态代理是由我们手动编写代理类,动态代理是在运行时由程序生成的代理类,二者的最终目的都是为了代理另外一个类(被代理类)的功能。以我们日常代购火车票为例,这里先简要了解下静态代理模式,方便理解动态代理。

静态代理模式

首先定义一个接口,声明被代理的类所需要实现的功能

/**
 * 买票接口
 */
public interface IBuy {
    //买票
    void buyTicket();
}

定义被代理类(火车站),实现买票功能:

/**
 * 被代理类
 */
public class TrainStation implements IBuy{

    private String destination;

    public TrainStation(String destination) {
        this.destination = destination;
    }

    @Override
    public void buyTicket() {
        Log.d("Proxy", "在火车站售票处买到了去" + destination + "的火车票");
    }
}

定义代理类,实现同一个接口:

/**
 * 票贩子 代理火车站卖票
 */
public class TrainStationProxy implements IBuy{

    private String destination;
    //持有火车站(相当于能直接访问火车站内部)
    private TrainStation trainStation;

    public TrainStationProxy(String destination) {
        this.destination = destination;
    }

    @Override
    public void buyTicket() {
        if(trainStation == null){
            trainStation = new TrainStation(destination);
        }
        //本质还是通过火车站买票
        trainStation.buyTicket();
        Log.d("Proxy", "通过票贩子买到了去"+ destination + "的火车票");
    }
}

客户端调用:

//客户端通过票贩子,间接买到了票
IBuy proxy = new TrainStationProxy("北京");
proxy.buyTicket();

通过上面的例子,可以看出票贩子就是一个火车站的代理,客户端本质上买的票其实还是火车站的,但是是通过票贩子来帮我们代理买票这个动作,以上就是静态代理模式。

 

为什么要使用动态代理?

静态代理模式实现了我们通过票贩子买票的功能,但票贩子这个代理类需要我们手动编码定义,如果需要代理的对象多了,比如有一百多个需要代理的功能(代理进站、代理托运、代理换乘...等等),那岂不是要建一百多个代理类,这个时候就需要我们的动态代理模式了,动态代理与静态代理最大的区别是,在运行时让虚拟机帮我们生成一个对应的代理类,来调用对应的方法,并且在使用结束后回收,解决了静态代理的局限性。
 


如何实现动态代理?

动态代理模式就不需要我们定义代理类了,但需要借助InvocationHandler这个类来实现,主要有如下几个步骤:

1.声明调用处理器类InvocationHandler
2.声明目标对象类的抽象接口
3.声明目标对象类
4.动态生成代理对象,通过代理对象来调用目标对象的方法

1.声明调用处理器类InvocationHandler
/**
 * Created by Y on 2019/2/25.
 */
public class DynamicProxy implements InvocationHandler {

    private Object proxyObject;

    public Object newProxyInstance(Object proxyObject) {
        this.proxyObject = proxyObject;
        return Proxy.newProxyInstance(proxyObject.getClass().getClassLoader(),
                proxyObject.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Log.d("Dynamic", "代理对象准备开始调用目标对象方法");
        Object result = method.invoke(proxyObject, args);
        Log.d("Dynamic", "代理对象调用目标对象方法完毕");
        return result;
    }
}

可以看到,newProxyInstance方法中,调用了Proxy.newPorxyInstance方法,方法参数如下:

Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h)

这个方法有什么用呢?先说下这几个参数的意义

ClassLoader loader:指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
Class<?>[] interfaces:指定目标对象的实现接口,即要给目标对象提供一组什么接口。若提供了一组接口给它,那么该代理对象就默认实现了该接口,这样就能调用这组接口中的方法
InvocationHandler h:指定InvocationHandler对象。即动态代理对象在调用方法时,会关联到哪个InvocationHandler对象

可以看到头两个对象传进去了目标类的ClassLoader对象,以及它所声明的接口,到时候程序运行起来就会在Proxy.newPorxyInstance内部通过反射来生成目标类的实例,并且提供一组接口给它实现,到时候外界需要这些接口的时候才能调用(就类似于刚才举例的Retrofit的ServiceApi.interface)。然后将我们的InvocationHandler对象传进去,代理对象在调用方法时内部就会使用该对象调用invoke(),进而回调回我们的invoke方法。

上面通过newProxyInstance生成代理实例,那invoke自然就是用来代理调用目标方法,method.invoke同样是代理类运用反射调用目标方法并返回结果。
 

2.声明目标对象类的抽象接口
public interface TestInterface {
    void test();
}

 

3.声明目标对象类
public class TestImpl implements TestInterface{

    @Override
    public void test() {
        Log.d("Android小Y", "调用了目标类的test方法");
    }
}

 

4.通过动态代理对象,调用目标对象的方法
// 1. 创建调用处理器类对象
DynamicProxy dynamicProxy = new DynamicProxy();

// 2. 创建目标对象对象
TestImpl testImpl = new TestImpl();

// 3. 通过调用处理器类对象newProxyInstance创建动态代理类对象
TestInterface proxy = (TestInterface) dynamicProxy.newProxyInstance(testImpl);
        
//代理调用test,从而调用到了invoke,最后调用到了目标类的test方法
proxy.test();

 
运行结果

02-15 15:48:30.099 6578-6578/com.example.test.main D/Android小Y: 代理对象准备开始调用目标对象方法
02-15 15:48:30.101 6578-6578/com.example.test.main D/Android小Y: 调用了目标类的test方法
02-15 15:48:30.101 6578-6578/com.example.test.main D/Android小Y: 代理对象调用目标对象方法完毕

可以看到,动态代理模式下,我们只需要将具体被代理类传给处理器,即可为我们动态生成对应的代理类,不再需要像静态代理那样繁琐。

 

Retrofit中的动态代理

用过Retrofit网络请求库的朋友都知道,Retrofit的基本用法就是:

1.先定义一个接口:

public interface ServerApi {
    @GET("xxxxx/xxx/xxx")
    Call<ResponseBean> getData();
}

2.接着创建Retrofit实例:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://xxx.xxx.com/") //设置网络请求的Url地址
                .addConverterFactory(GsonConverterFactory.create()) //设置数据解析器
                .build();

3.发起请求

ServerApi request = retrofit.create(ServerApi.class); //创建代理实例
request.getData(); //发送请求

注意第三步骤,表面上,传进去我们接口的类对象即可生成对应的实例并调用getData,是因为这里面实际上retrofit利用动态代理模式生成了一个代理对象(并且实现了ServiceApi),然后一旦调用了ServiceApi的方法,就会触发代理对象的invoke方法,从而在invoke总拦截并获得了ServiceApi的所有方法以及对应的那些@GET @POST 等Retrofit的注解信息,然后利用OKhttp完成真正的请求操作。

我们从Retrofitcreate方法一探究竟:

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod serviceMethod = loadServiceMethod(method);
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
}

可以看到,也是通过Proxy.newProxyInstance生成了代理类,并new了一个匿名内部InvocationHandler对象,重写它的invoke方法,相当于通过我们传进来的接口,生成了一个接口的代理类,并且实现了接口是所有方法(因为InvocationHandler的参数里传进去了new Class<?>[] { service }),然后返回给外部。
外部一旦调用了接口方法,例如request.getData();,就会触发InvocationHandlerinvoke,从而根据注解生成Retrofit的ServiceMethodOkHttpCall对象,这些都是发起网络请求的必备信息,然后最终发起请求。

那么Retrofit为何要这么设计呢?

我们一个应用的请求接口可能有很多个,通过动态代理模式,能动态为每个接口都生成一个具体的代理类,并且实现了我们的接口,我们不需要关心具体请求细节是怎样的,只需要声明我们的接口并传递给Retrofit即可,然后由Retrofit动态生成具体请求对象,发起请求并将结果返回给我们,我想这也就是为何Retrofit这么受欢迎的原因之一。

 

总结

综上,一般为了在不修改原有类的情况下扩展其功能,并且保护被代理类不被访问,我们可以选择代理模式。动态代理模式是通过动态生成代理类的代理模式,它其实就是将目标类的类加载器和相应的接口传进去代理器(InvocationHandler),通过代理器生成了一份新的”备份“——代理类,这个动态生成的代理类里实现了接口中的所有方法,然后在 InvocationHandler.invoke()中通过反射机制,调用目标类对象的方法。动态代理运用了很多反射,在性能上会有所影响,所以并不是随处滥用,用得得当会有很好的效果,比如常说的AOP切面编程,实现无侵入式的代码扩展。
 

GitHubGitHubZJY
CSDN博客IT_ZJYANG
简 书Android小Y

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容