作者:网易合作产品部·李若昆

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


引入Retrofit

这个第三方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是对请求返回数据的定义,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函数,会返回一个CallAdapte,至于是个什么样的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。

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

·END·

推荐阅读

Android View的事件分发机制解析

跨语言编程问题迎刃而解的3个要点

网易云信∣真正稳定的IM云服务

http://netease.im  长按识别,关注精彩

别人家SDK的设计模式——Android Retrofit库源码解读相关推荐

  1. PackageManagerService Android 8.1 源码解读 02

    接上文:PackageManagerService Android 8.1 源码解读 01 d.第三步细节:PKMS.main(),main函数主要工作: [检查]Package编译相关系统属性 [调 ...

  2. PackageManagerService Android 8.1 源码解读 01

    一.PackageManagerService 是什么? 答: PackageManagerService(简称 [PKMS]),是 Android 系统中核心服务之一,负责应用程序的安装,卸载,信息 ...

  3. 【Android】OkHttp源码解读逐字稿(2)-OkHttpClient

    目录 0.前言 1.各个属性浅析 01.dispatcher 02.connectionPool 03.interceptors&networkInterceptors 04.eventLis ...

  4. android 普通蓝牙源码解读

    一:概述 上一篇博客讲了下蓝牙4.0在android中的应用,这里讲讲普通蓝牙之间如何操作. 我记得在大二的时候还做了个比较烂的游戏,当时喜欢打dota就做了个蓝牙之间对战坦克的游戏,可以去看看,确实 ...

  5. 【Android】OkHttp源码解读逐字稿(1)-拦截器

    目录 0.前言 1.OkHttp的简单使用 2.浅析开始 拦截器 链式调用流程示意图 第 0 个拦截器 第一个 拦截器 RetryAndFollowUpInterceptor 第二个拦截器 Bridg ...

  6. libco协程库源码解读

    2019独角兽企业重金招聘Python工程师标准>>> 协程,又被称为用户级线程,是在应用层被调度,可以减少因为调用系统调用而阻塞的线程切换的时间.目前有很多协程的实现,由于微信内部 ...

  7. android 三方_面试官送你一份Android热门三方库源码面试宝典及学习笔记

    前言 众所周知,优秀源码的阅读与理解是最能提升自身功力的途径,如果想要成为一名优秀的Android工程师,那么Android中优秀三方库源码的分析和理解则是必备技能.就拿比较热门的图片加载框架Glid ...

  8. android 4.4 源码编译,ubuntu12.04(64bit)编译android4.4源码、sdk及kernel

    最近闲来无事,就想编译一下android源码折腾折腾.过程还算顺利,编译源码用了近3小时,编译sdk用了近30分钟.关于为什么要编译4.4的源码,而不编译5.0.6.0的源码,一是正好笔记本中有下载好 ...

  9. Android主流三方库源码分析(九、深入理解EventBus源码)

    一.EventBus使用流程概念 1.Android事件发布/订阅框架 2.事件传递既可用于Android四大组件间通信 3.EventBus的优点是代码简洁,使用简单,事件发布.订阅充分解耦 4.首 ...

最新文章

  1. 中山行书百年纪念版字体可以商用吗_干货|免费可商用字体
  2. Linux 使用ps命令查看某个进程文件的启动位置
  3. Python os 属性(便于跨平台开发)
  4. 模型训练慢和显存不够怎么办?GPU加速混合精度训练
  5. 梁胜:开源是最好的商业模式
  6. pymongo操作方法
  7. 手绘流程图讲解spark是如何实现集群的高可用
  8. c语言标准课程方案,《C语言程序设计》课程标准方案.doc
  9. Socket 核心原理分享
  10. 做自媒体和有没有文化没有太大关系
  11. Flutter实战一Flutter聊天应用(六)
  12. 谷歌发布 Linux 内核提权漏洞奖励计划,综合奖金最高超30万美元
  13. OpenSceneGraph FAQ
  14. c语言投票程序设计,《C语言及程序设计》实践参考——投票表决器
  15. Prewitt算子计算图像梯度
  16. springcloud服务假死
  17. 求旋转中心【halcon定位】
  18. 大数据之足球盘口赔率凯利必发数据采集爬虫
  19. 诊断2018微商:从乱象到赋能,世界微商大会发明的新玩法为您指路
  20. VS调试C++程序,提示无法启动程序,“xx.exe”。系统找不到指定文件的解决办法

热门文章

  1. Linux驱动设计——字符杂项设备
  2. 标准配置的UBUNTU 11.10 RUBY VMWARE 镜像,手工MOD(ZSH_RVM_RAILS_VIM)
  3. sql中 set 和select 的区别
  4. SL2_RedSkin
  5. LncRNADisease database数据库使用方法
  6. kompozer+mysql_KompoZer for mac下载_KompoZer for mac版V0.8b3下载(暂未上线)_预约_飞翔下载...
  7. excel流程图分叉 合并_Excel和Visio联姻,自动生成跨职能流程图,还能用图标标记状态,太牛了!...
  8. uni微信小程序 下载图片跟文字_微信小程序:图片与文字无法居中 最后解决的方法是——...
  9. 鸿蒙比不了IOS,比苹果的iOS14还流畅,华为鸿蒙2.0可以啊!
  10. 基于倒谱法、自相关法、短时幅度差法的基音频率估计算法(MATLAB及验证)