androidRetrofit程序员

别人家SDK中的设计模式--Android Retrofit库源

2016-08-01  本文已影响714人  RockerLee

我们在日常编写代码中免不了会用到各种各样第三方库,网络请求、图片加载、数据库等等。有些lib接入可能方便到几行代码搞定,有些lib可能从demo、文档到测试都是坑(比如lib嵌套lib导致资源冲突、lib中定义的类无法扩展、兼容性差导致大量崩溃等),相信接过第三方库的童鞋不会没有过这样的吐槽。笔者也是在最近修改一个第三方lib的bug过程中翻看了一些源码,发现其中存在点设计技巧,于是结合最近看的设计模式,来讨论一下在SDK中如何使用,与大家相互交流,也为本人之后SDK的开发工作做点铺垫。

这个第三方lib叫做Retrofit,是个用在Java中支持restful的网络库。Retrofit是在基于OkHttp3的基础上,用动态代理和annotation实现了restful标准的规范,令开发者使用起来异常方便。Retrofit当然也实现了网络请求的异步处理,并且用工厂模式给开发者预留了很大的扩展空间,可以与ReactiveX结合,也可以由开发者定义自己的同步或异步请求、回调方式。

为了方便讲解设计模式的实现,我们先来看看代码中如何使用Retrofit。引用官方文档的介绍,只需要这样声明好你的api接口:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

在初始化时传入这个接口的class:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

调用接口时只需两行代码即可:

Call<List<Repo>> repos = service.listRepos("octocat”);//获取网络请求实例
repos.excute();//执行请求,异步请求用repos.enqueue(callback);

其中List<Repo>是对请求返回数据的定义,repos是执行请求的实例(实现了Call接口,后面会详细介绍)。

从以上代码可以看到,我们做的仅仅是声明了一个接口,涵盖所需的api接口,Retrofit就自动帮我们创建了一个实现这个api接口的实例,我们只需坐享其成调用实例的方法即可完成网络请求。Retrofit的这种“智能”是如何实现的呢?那就是接下来要谈的动态代理模式。

Retrofit中的代理模式

为什么需要代理呢?代理其实就是我们想做一件事的时候不亲自动手,也就是“创建网络请求实例”这件事,交给一个代理去创建,这样不管它内部怎样实现,只要能帮我们创建出一个可用的实例就可以了,通常这个实例也是实现了某个接口的(比如文中的Call接口),所以即使底层的实现改变,或者创建过程改变,使用者的代码是不需要调整的。就像我们在携程、去哪儿上买机票,我们也不关心他们到底是从航空公司官方买票,还是从中间商手中买票,只要最终我们能拿到票就行了(所以也会买到用里程数换来的机票,噗…)。

言归正传,Retrofit用到的动态代理,类图如下:



篮框中的就是代理部分,代理了用户定义接口(即开头实例中的GitHubService)中的所有函数,返回一个Call对象,代理实例通过这句代码来产生:

GitHubService service = retrofit.create(GitHubService.class);

进去看create函数源码会发现这里是通过反射实现的,直接返回了java.lang.reflect.Proxy中的方法newProxyInstance:

public <T> T create(final Class<T> service) {
    ...
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {

          @Override public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {
            //这里有判断method是否为Object类声明的方法
            ...
            ServiceMethod serviceMethod = loadServiceMethod(method);
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

这个代理实例可以将接口(也就是我们定义的GitHubService)指定的所有方法都指派到invocationHandler中去,当调用service的接口方法时,就会执行InvocationHandler中的Invoke方法,可以看到Retrofit就是在这里创建一个网络请求实例OkHttpCall,将其返回(其实返回的是callAdapter.adapt(okHttpCall),将okHttpCall适配转换过的对象,详见后面适配器模式),我们就可以利用此实例进行网络请求了。这里invoke方法三个参数中proxy就是代理对象,method表示要调用的方法,args是对method方法传入的参数。

Retrofit中的适配器模式

适配器模式是将一个类的接口,转换成客户期望的另一个接口,让原本接口不兼容的类可以合作无间。比如生活中的电源适配器,将220v电压转换成电子设备需要的输入电压,比如Android中的ListView,Adapter将各种各样的数据转换后传给ListView用来显示。Retrofit中的Adapter是用来转换网络请求Call接口的,而这里的Adapter可以由使用者自定义,从而转换成使用者希望的类,具有很强的扩展性,见类图:



图中绿色部分就是适配器模式。这个适配器是怎样运作的呢?

在刚才的代理模式中Retrofit已经帮我们智能创建了网络请求实例Call,Call是对网络请求定义的接口。Retrofit实际默认new的对象是OkHttpCall(一个封装了okhttp3.Call的类),我们并不在意它具体是什么类,能按照Call接口的定义来使用就够了。但用起来才发现我们会有很多额外需求,比如OkHttpCall的回调函数是在工作线程调用的,而网络回调函数我们通常要更新UI,再用handler转到主线程?对使用者来说太麻烦了。于是适配器华丽登场,CallAdapter可以将默认生成的OkHttpCall转换成你想要的任何类型。

比如Retrofit默认提供的Adapter,就是这样将OkHttpCall适配成ExecutorCallbackCall:

new CallAdapter<Call<?>>() {
  ...
  @Override public <R> Call<R> adapt(Call<R> call) {
    return new ExecutorCallbackCall<>(callbackExecutor, call);
  }
}

static final class ExecutorCallbackCall<T> implements Call<T> {
  ...
  @Override public void enqueue(final Callback<T> callback) {
    delegate.enqueue(new Callback<T>() {
      @Override public void onResponse(Call<T> call, final Response<T> response) {
        callbackExecutor.execute(new Runnable() {
          ...
        });
      }
    });
  }
}

可以看到ExecutorCallbackCall在enqueue方法中,添加了一层回调,用自定义线程(通常就是主线程)执行器执行外部callback,而在CallAdapter.adapt函数直接返回ExecutorCallbackCall的新实例就可以了,也就是动态代理中提到的这句:

return serviceMethod.callAdapter.adapt(okHttpCall);

这样在适配器的帮助下既可以增强扩展添加新功能,又不会增加使用者代码量。比如你希望在网络回调时统一处理一些错误码,或者希望与RxJava结合使用,又或者希望单独处理cancel函数等等。这些都可以通过适配器来将Retrofit返回的Call适配成你想要的类。

然而还存在个问题,适配器的adapt方法是在Retrofit内部调用的,它怎么知道使用者要用哪个或哪几个适配器呢?使用者如何设置自己的适配器呢?这就引出了下面要介绍的工厂模式。

Retrofit中的工厂模式

工厂模式分为简单工厂模式、工厂方法模式和抽象工厂模式。应用场景大部分是需要根据不同类型来生成不同对象时使用。刚接触工厂模式时,以为这三种模式一个比一个高级,是层层递进的关系。然而并不是,简单工厂模式的确是最简单的一种,但工厂方法模式和抽象工厂模式应该属于平级,只是为了解决不同维度的问题而存在。

简单工厂模式就是依据变化封装的原则,将生产对象的部分封装在工厂内部,根据不同需求返回不同类型实例,结构简单但扩展起来麻烦,需要对工厂类进行修改。因此生产的类型一旦变多,就需要工厂方法模式了,将工厂定义成一个接口(或抽象类),每新增一类产品就新增一个工厂实例即可,完全符合开放关闭原则,满足大多数情况的需求。而抽象工厂模式适用于多个产品树的情况,比如原本工厂方法模式可以生产轿车、越野车和跑车,但这时候新增了一个产品树:电动轿车、电动越野车和电动跑车,就需要用到抽象工厂模式了,但这种模式对新增产品族,比如新增了商务车,修改起来较复杂。

上面谈了适配器adapter的作用,而适配器的产生就是由工厂模式来完成的,见类图:



图中红框就是工厂方法模式,CallAdapter的生产由CallAdapter.Factory这个接口定义,包含了一个get函数,会返回一个CallAdapter,至于是个什么样的CallAdapter则由子类来实现。比如上面讲适配器时提到的将OkHttpCall转换成ExecutorCallbackCall的适配器,就是由这个ExecutorCallAdapterFacotry生产的。工厂方法模式重点就在于将方法抽象为接口或父类,利用继承关系和子类的差异化创建不同的Adapter,从而将默认生成的OkHttpCall转换成你所需要的各种类型。

谈了这么多还是感觉不到这些设计模式的作用吗?没关系,来看下我们拓展后的类图:



图中灰色的就是默认的和扩展的工厂模块。除了Retrofit默认提供的ExecutorCallAdapterFactory和ExecutorCallbackCall,我们可以扩展出自己的Call和Factory,比如图中的GACall和GACallAdapterFacotry,我这里扩展的GACall修改了cancel()的行为,调用cancel()之后就会切除callback在IO线程中的引用,不再收到回调,从而方便处理页面销毁后网络请求才收到返回的情形。当然你还能扩展出其他Factory、Call和Callback(比如RxJava对Retrofit专门实现了一个Factory,直接拿来用就行了),只要记得将你的Factory添加到Retrofit类的adapterFactories列表中就行。

但用户添加了这么多工厂,真正生产网络请求实例时,要用哪个工厂呢?仔细看工厂接口的get方法:

public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit);

第一个参数是returnType,也就是网络请求返回的数据类型:

public interface GitHubService {
  @GET("users/{user}/repos")

  Call<List<Repo>> listRepos(@Path("user") String user);

  @GET("users/{user}/repos")
  GACall<List<Repo>> listRepos2(@Path("user") String user);
}

上面请求声明的返回类型分别是Call和GACall,工厂会根据传入的returnType来分辨是否属于自己的生产范围,于是returnType为Call就会由Retrofit默认的工厂生产Adapter,returnType为用户自定义的类型(如GACall),则由用户定义的工厂(如GACallAdapterFacotry)生产Adapter。

以上就是本人在修改内存泄露导致崩溃的bug时,碰巧看到Retrofit源码比较有趣,分析了一遍拿来和大家分享。大体思路就是先用反射代理帮用户生产请求实例,再由适配器转换成用户期望的类型,而这个适配器是通过工厂方法模式让用户无限扩展和自定义的。其实深究下去里面还有很多设计模式的体现,这次就先挑这三种具有代表性的好了。只要我们留意身边的源代码,就会发现别人巧妙的设计无处不在。

上一篇下一篇

猜你喜欢

热点阅读