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

我们在日常编写代码中免不了会用到各种各样第三方库,网络请求、图片加载、数据库等等。有些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源码比较有趣,分析了一遍拿来和大家分享。大体思路就是先用反射代理帮用户生产请求实例,再由适配器转换成用户期望的类型,而这个适配器是通过工厂方法模式让用户无限扩展和自定义的。其实深究下去里面还有很多设计模式的体现,这次就先挑这三种具有代表性的好了。只要我们留意身边的源代码,就会发现别人巧妙的设计无处不在。

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

推荐阅读更多精彩内容