前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从架构角度看Retrofit的作用、原理和启示

从架构角度看Retrofit的作用、原理和启示

作者头像
我就是马云飞
发布2018-02-05 11:24:39
2K0
发布2018-02-05 11:24:39
举报
文章被收录于专栏:我就是马云飞我就是马云飞

前言

Retrofit是squareup公司的开源力作,和同属squareup公司开源的OkHttp,一个负责网络调度,一个负责网络执行,为Android开发者提供了即方便又高效的网络访问框架。

不过,对于Retrofit这样设计精妙、代码简洁、使用方便的优秀开源项目,不能仅知道如何扩展和使用,或者仅研究它采用的技术或模式,“技”当然重要,但不能忽视了背后的“道”。

对于Retrofit,我们还应该看到的,是她在优化App架构方面的努力,以及她在提升开发效率方面的借鉴和启示。

本文试图通过一个具体场景,先总结Retrofit在架构中起到的作用,再分析其实现原理,最后探讨Retrofit给我们带来的启示。

我们先通过一个简单的应用场景来回顾Retrofit的使用过程。

基本场景

通常来说,使用Retrofit要经过这样几个步骤

  1. 引用在gradle文件中引用retrofit
代码语言:javascript
复制
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.retrofit2:retrofit-converters:2.3.0'
compile 'com.squareup.retrofit2:retrofit-adapters:2.3.0'

如果需要使用更多扩展功能,比如gson转换,rxjava适配等,可以视自己需要继续添加引用

代码语言:javascript
复制
 compile 'com.squareup.retrofit2:converter-gson:2.3.0'
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'

如果现有的扩展包不能满足需要,还可以自己扩展converter,adapter等。

  1. 定义接口Retrofit要求定义一个网络请求的接口,接口函数里要定义url路径、请求参数、返回类型。 public interface INetApiService { @GET("/demobiz/api.php") Call<BizEntity> getBizInfo(@Query("id") String id); }

在这个接口定义中,用注解@GET("/demobiz/api.php")声明了url路径,用注解@Query("id") 声明了请求参数。 最重要的是,用Call<BizEntity>声明了返回值是一个Retrofit的Call对象,并且声明了这个对象处理的数据类型为BizEntity,BizEntity是我们自定义的数据模型。

  1. 依次获得Retrofit对象、接口实例对象、网络工作对象首先,需要新建一个retrofit对象。然后,根据上一步的接口,实现一个retrofit加工过的接口对象。最后,调用接口函数,得到一个可以执行网络访问的网络工作对象。

这个复杂的过程下来,最终得到的callWorker对象,才可以执行网络访问。

  1. 访问网络数据用上一步获取的worker对象,执行网络请求

在回调函数里,取得我们需要的BizEntity数据对象。网络访问结束。

角色与作用

我们从上面的应用场景可以看出,Retrofit并不做网络请求,只是生成一个能做网络请求的对象。Retrofit的作用是按照接口去定制Call网络工作对象

什么意思?就是说:Retrofit不直接做网络请求Retrofit不直接做网络请求Retrofit不直接做网络请求重要的事情说三遍。

网络请求的目标虽然是数据,但是我们需要为这个数据写大量的配套代码,发起请求的对象Call,接收数据的对象CallBack,做数据转换的对象Converter,以及检查和处理异常的对象等。这对于一个项目的开发、扩展和维护来说,都是成本和风险。

而Retrofit做的事情,就是为开发者节省这部分的工作量,Retrofit一方面从底层统一用OkHttp去做网络处理;另一方面在外层灵活提供能直接融入业务逻辑的Call网络访问对象。

具体来说,Retrofit只负责生产对象,生产能做网络请求的工作对象,他有点像一个工厂,只提供产品,工厂本身不处理网络请求,产品才能处理网络请求。Retrofit在网络请求中的作用大概可以这样理解:

我们看到,从一开始,Retrofit要提供的就是个Call工作对象。换句话说,对于给Retrofit提供的那个接口

代码语言:javascript
复制
public interface INetApiService {
    @GET("/demobiz/api.php")
    Call<BizEntity> getBizInfo(@Query("id") String id);
}

这个接口并不是传统意义上的网络请求接口,这个接口不是用来获取数据的接口,而是用来生产对象的接口,这个接口相当于一个工厂,接口中每个函数的返回值不是网络数据,而是一个能进行网络请求的工作对象,我们要先调用函数获得工作对象,再用这个工作对象去请求网络数据。

所以Retrofit的实用价值意义在于,他能根据你的接口定义,灵活地生成对应的网络工作对象,然后你再择机去调用这个对象访问网络。理解了这一点,我们才能去扩展Retrofit,并理解Retrofit的设计思想。

功能扩展

我们先来看Retrofit能扩展哪些功能,然后再去理解Retrofit的工作原理。Retrofit主要可以扩展三个地方:

1.OkHttpClientRetrofit使用OkHttpClient来实现网络请求,这个OkHttpClient虽然不能替换为其他的网络执行框架比如Volley,但是Retrofit允许我们使用自己扩展OkHttpClient,一般最常扩展的就是Interceptor拦截器了

2.addConverterFactory

扩展的是对返回的数据类型的自动转换,把一种数据对象转换为另一种数据对象。在上述场景中,GsonConverterFactory可以把Http访问得到的json字符串转换为Java数据对象BizEntity,这个BizEntity是在INetApiService接口中要求的的。这种转换我们自己也经常做,很好理解。如果现有的扩展包不能满足需要,可以继承Retrofit的接口。retrofit2.Converter<F,T>,自己实现Converter和ConverterFactory。在创建Retrofit对象时,可以插入我们自定义的ConverterFactory。

代码语言:javascript
复制
//retrofit对象
Retrofit retrofit=new Retrofit.Builder()
.baseUrl(Config.DOMAIN)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(YourConverterFactory.create())//添加自定义Converter
.build();

3.addCallAdapterFactory

扩展的是对网络工作对象callWorker的自动转换,把Retrofit中执行网络请求的Call对象,转换为接口中定义的Call对象。

这个转换不太好理解,我们可以对照下图来理解:

Retrofit本身用一个OkHttpCall的类负责处理网络请求,而我们在接口中定义需要定义很多种Call,例如Call<BizEntity>,或者Flowable<BizEntity>等,接口里的Call和Retrofit里的OkHttpCall并不一致,所以我们需要用一个CallAdapter去做一个适配转换。 (Retrofit底层虽然使用了OkHttpClient去处理网络请求,但她并没有使用okhttp3.call这个Call接口,而是自己又建了一个retrofit2.Call接口,OkHttpCall继承的是retrofit2.Call,与okhttp3.call只是引用关系。这样的设计符合依赖倒置原则,可以尽可能的与OkHttpClient解耦。)

这其实是Retrofit非常核心,也非常好用的一个设计,如果我们在接口中要求的函数返回值是个RxJava的Flowable对象

代码语言:javascript
复制
public interface INetApiService {
    @GET("/demobiz/api.php")
    Flowable<BizEntity> getBizInfo(@Query("id") String id);
}

那么我们只需要为Retrofit添加对应的扩展

代码语言:javascript
复制
//retrofit对象
Retrofit retrofit=new Retrofit.Builder()
.baseUrl(Config.DOMAIN)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();

就能得到Flowable类型的callWorker对象

代码语言:javascript
复制
//用retrofit加工出对应的接口实例对象
INetApiService netApiService= retrofit.create(INetApiService.class);
···
//调用接口函数,获得网络工作对象
Flowable<BizEntity> callWorker= netApiService.getBizInfo("id001");

在这里,callAdapter做的事情就是把retrofit2.Call对象适配转换为Flowable<T>对象。

同样,如果现有的扩展包不能满足需要,可以继承Retrofit的接口retrofit2.CallAdapter<R,T>,自己实现CallAdapter和CallAdapterFactory。

实现原理

Retrofit固然设计精妙,代码简洁,使用方便,但相应的,我们要理解Retrofit的实现原理也不太容易,这么精妙的设计是极佳的研究素材,我们不能仅仅停留在知道怎么使用,怎么扩展的阶段,那实在是对这个优秀开源项目的浪费。其实,Retrofit使用的,就是动态代理,方法注解、建造者和适配器等成熟的技术或模式,但是由于她的设计紧凑,而且动态代理屏蔽了很多过程上的细节,所以比较难以理解。 动态代理

从前面的使用场景可知,retrofit会生成一个接口实例。

代码语言:javascript
复制
//用retrofit加工出对应的接口实例对象
INetApiService netApiService= retrofit.create(INetApiService.class);

到Retrofit源码里看create函数,是一个动态代理。

要理解动态代理,最好要看到动态生成的代理类。

由于动态代理是在运行时动态生成的代理类,用常规的反编译方法无法查看,一般要使用Java提供的sun.misc.ProxyGenerator.generateProxyClass(String proxyName,class[] interfaces)函数生成代理类,函数会返回byte[]字节码,然后对字节码反编译得到Java代码。有一个小问题是,AndroidStudio并不提供sun.misc这个包,我们需要用IntelliJ或者Eclipse建立一个Java工程,在Java环境里调用这个函数。

拿到的代理类,大概是这样的:

我们可以看到,代理类生成的是一个INetApiService接口的实例对象,该对象的getBizInfo函数返回的是接口中定义的Call网络工作对象,这也体现了Retrofit的核心价值,生成接口定义的Call网络工作对象。

那么,这个Call网络工作对象是如何生成的呢,上面动态代理生成的代码是这样的:

return (Call)this.h.invoke(this, m3, new Object[]{str});

也就是说,这个Call网络工作对象是在InvocationHandler中实现的,也就是在Retrofit.create函数中,由InvocationHandler实现的。

这样我们就明白了,Retrofit使用动态代理,其实是为了开发者在写代码时方便调用,而真正负责生产Call网络工作对象的,还是Retrofit.create函数中定义的这个InvocationHandler,这个InvocationHandler的代码我们再贴一遍:

ServiceMethod能让我们准确解析到INetApiService中定义的函数,为最后的适配转换提供转换目标,详细分析我们后面再说,先看适配转换的过程。

我们看到,Retrofit内部默认使用OkHttpCall对象去处理网络请求,但是返回的网络工作对象是经过适配器转换的,转换成接口定义的那种Call网络工作对象。

这个适配转换,就是Retrofit能按照接口去定制Call网络工作对象的秘密。 适配转换call对象

我们在初始化Retrofit对象时,好像不添加CallAdapterFactory也能实现适配转换。

这是怎么回事呢,我们知道Retrofit使用了建造者模式,建造者模式的特定就是实现了建造和使用的分离,所以建造者模式的建造函数里,一般会有很复杂的对象创建和初始化过程,所以我们要看一下Retrofit的build函数。

这段代码里,我们看到Retrofit使用OkHttpClient处理网络请求,并且会添加默认的callAdapterFactory,这个platform是一个简单工厂,能根据当前系统平台去生成对应的callAdapterFactory

这个Platform是Retrofit在Builder的构造函数里初始化的。

所以,在Retrofit.build()函数中,我们为Retrofit默认添加的callAdapterFactory,是在Platform中为Android系统设定的ExecutorCallAdapterFactory。我们看ExecutorCallAdapterFactory的代码,这是一个工厂类,可以返回CallAdapter对象:

在adapt函数中,适配器会把Retrofit中用来访问网络的OkHttpCall,转换为一个ExecutorCallbackCall(继承了INetApiService接口里要求返回的网络工作对象retrofit2.Call),

这个例子里面,由于OkHttpCall和ExecutorCallbackCall都实现了retrofit2.Call接口,结果出现了从Call<Object>转换为Call<Object>的情况,这可能不容易理解,我们换个RxJava2CallAdapterFactory来看看

这个CallAdapter的转换就比较明显了,把retrofit2.Call对象通过适配器转换为了一个实为Observable<?>的Object对象。

至此,我们可以理解Retrofit根据接口定义动态生产Call网络请求工作对象的原理了,其实就是通过适配器把retrofit2.Call对象转换为目标对象。

至于适配器转换过程中,如何实现的对象转换,就可以根据需求来自由实现了,比如利用静态代理等,如有必要,我们可以自行开发扩展,Retrofit框架并不限制我们对于适配器的实现方式。 函数解析,网络请求,数据转换

在前面分析中,我们知道了Retrofit的整体工作流程,就是Retrofit用动态代理生成Call网络请求对象,在这个过程中,用适配器把Retrofit底层的retrofit2.Call对象转换为INetApiService中定义的Call网络请求对象(如Flowable)。

问题是,Retrofit具体是如何知道了INetApiService中定义的Call网络请求对象,如何实现网络请求,以及如何执行的数据转换呢?

具体过程如下;首先,根据INetApiService中定义的函数,解析函数,得到函数的具体定义,并生成对应的ServiceMethod。然后,根据这个ServiceMethod,实现一个OkHttpCall的Call对象,负责在Retrofit底层实现网络访问。其中,在网络访问返回了网络数据时,根据ServiceMethod实现数据转换。最后,利用上一小节中匹配的适配器,把OkHttpCall对象转换为INetApiService要求的Call网络请求对象。

所以,我们要了解的就是函数解析、网络请求和数据转换这三个动作,至于最后的适配转换,在上一节中已经分析过了。

1. 函数解析在接口函数里,用注解描述了输入参数,用Java对象定义了返回值类型,所以对输入参数和返回值,ServiceMethod采取了不同的方式去处理。输入参数输入参数是用来描述url的,它的处理相对简单,ServiceMethod会根据反射得到的Method,取得Annotation注解信息,这些注解是Retrofit自己预定义好的(retrofit2.http.*),ServiceMethod根据预先的定义,直接判断注解所属的逻辑分支,在有网络请求时分情况进行处理,就能得到目标url,http请求头等数据。返回值返回值是需要用CallAdapter去适配的,所以核心在于生成对应的CallAdapter。在Retrofit生成Call网络工作对象时,她通过动态代理获取到了接口函数的Method定义,从这个Method中可以获取函数定义的返回对象类型,由于这个转换是需要CallAdapterFactory生产CallAdapter对象去实现,而Retrofit事先并不知道要使用哪个Factory,所以她是遍历所有的CallAdapterFactory,根据目标函数的返回值类型,让每个Factory都去尝试生产一个CallAdapter,哪个成功就用哪个。

2. 网络请求OkHttpCall继承的retrofit2.Call接口是为了依赖倒置解耦的,真正的网络请求是由OkHttpCall内部引用的okhttp3.call处理的,这个okhttp3.call是借道ServiceMethod获取的Retrofit中的callFactory,也就是Retrofit中的OkHttpClient。

整个引用链条是这样的:OkHttpCall--okhttp3.call-->ServiceMethod--callFactory-->Retrofit.build()--callFactory//(如未扩展赋值)new OkHttpClient();-->Retrofit.Builder().client(mClient)//(可能有扩展赋值)扩展过的OkHttpClient

最终的网络请求是由OkHttpCall调用OkHttpClient发出的,调用和回调等过程,也就是在OkHttpCall中处理的。

网络请求的生成过程中,为了使用接口函数中定义的参数,OkHttpCall会调用ServiceMethod来生成Request请求对象,再交给OkHttpCall去处理。

3. 数据转换因为回调是在OkHttpCall中处理的,所以对回调数据的转换也在OkHttpCall中触发,为了符合接口函数中定义的返回数据类型,OkHttpCall会调用ServiceMethod来转换Response返回数据对象。

OkHttpCall对返回的网络数据,会调用一个serviceMethod.toResponse(ResponseBody body)函数,函数中执行的是:

代码语言:javascript
复制
R toResponse(ResponseBody body) throws IOException {
    return responseConverter.convert(body);
  }

这个函数可以把原始的okhttp3. ResponseBody数据转换为INetApiService接口中要求的数据类型(如BizEntity类型)。

从代码可以看出,实现数据转换的核心对象其实是responseConverter,这个Converter实际上要依次经过Retrofit的建造和ServiceMethod的建造后,才能确定下来的。

Retrofit建造时添加数据转换工厂Retrofit里有converterFactries列表,这是在我们初始化Retrofit实例时添加的

代码语言:javascript
复制
//retrofit对象
Retrofit retrofit=new Retrofit.Builder()
.baseUrl(Config.DOMAIN)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(YourConverterFactory.create())//添加自定义Converter
.build();

ServiceMethod建造时设定数据转换器

ServiceMethod在建造时,就已经确定了对应的是INetApiService中的哪个函数,所以需要明确设定自己的Converter<R,T>转换对象

这需要调用Retrofit

Retrofit会在自己的转换器工厂列表中遍历每个ConverterFactory,尝试根据ServiceMethod所对应的目标数据类型,找到Converter数据转换类

以Gson转换为例,GsonConverterFactory会通过getAdapter来尝试匹配目标数据类型:

public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {...}

如果可以匹配,那么前面调用serviceMethod.toResponse(ResponseBody body)函数时,会调用

在调用这段代码时,其实就是调用了Gson中最终执行数据转换的代码:

总结来说,Retrofit在类的单一职责方面分隔的很好,OkHttpCall类只负责网络交互,凡是需要知道函数定义的,都交给ServiceMethod类去处理,而ServiceMethod类对使用者不公开,因为Retrofit是个外观模式,而所有需要扩展的都在Retrofit的建造者中实现,他们的分工大概是这样的:

这三个类分工合作,共同实现了函数解析、网络访问和数据转换,并保留了良好的可扩展性。 整体结构与分析

至此,Retrofit的实现细节就已经基本清楚了,他用动态代理去定制接口定义的Call网络工作对象,用适配器去把底层的Call对象转换为目标Call对象,用函数解析/OkHttpClient/数据转换等实现对Call对象的适配转换,并能处理真正的网络请求。这里面涉及的整体结构和角色分工,大概可以这样表示:

其中,扩展适配器、扩展数据转换和扩展OkHttpClient,虽然都是通过Retrofit实现扩展,但真正的使用者是Retrofit内部的ServiceMethod、OkHttpCall和okhttp3.call等类或对象。

反推retrofit的设计

如果我们不直接正面分析Retrofit的结构设计和技术细节,而是先从Retrofit的功能和作用入手,倒过来推测Retrofit的目标,进而分析其架构和搭建细节,Retrofit为什么会设计成这样就很好理解了。

Retrofit的功能是按照接口定义,自动定制Call网络工作对象,所以Retrofit的目标应该就是避免为网络访问开发大量的配套代码。

为了实现这一目标,Retrofit需要分析哪些是易变的,哪些是不变的,然后分别处理。

由于Retrofit提供网络访问的工作对象,又是服务于具体业务,所以可以分网络访问和具体业务两部分来分析。

网络访问的不变性对于网络访问来说,不变的是一定有一个实现网络访问的对象,Retrofit选用了自家的OkHttpClient,不过为了把Retrofit和OkHttp两个项目解耦合,Retrofit根据依赖倒置原则,定义了Retrofit自己的Call即retrofit2.call,并定义了操作网络请求的OkHttpCall

网络访问的易变性对于网络访问来说,易变的是网络访问的url、请求方式(get/post等)、Http请求的Header设置与安全设置等,以及返回的数据类型。

针对易变的url和请求方式,Retrofit使用了方法注解的方式,可读性良好,扩展性优异,但这需要实现对接口函数中注解的解析,这样就有了ServiceMethod。针对Http请求的各种设置,其实Retrofit没做什么,因为Retrofit使用的OkHttp有拦截器机制,可以应付这种变化。针对返回的数据类型,由于目标数据类型与业务有关,是不确定的,Retrofit无法提供一个万能的转换类,所以Retrofit提供了扩展接口,允许开发者自己定义ConverterFactory和Converter,去实现潜在的数据类型转换。

具体业务的不变性对于具体业务来说,不变的是一定要有一个Call网络工作对象,所以Retrofit可以有一个生产对象的机制(像工厂一样)

具体业务的易变性对于具体业务来说,易变的就是这个Call网络工作对象的类型,不仅有CallBacl回调、可能还有Flowable工作流、或者其他潜在的对象类型。

针对这种Call对象的易变性,Retrofit也是无法提供一个万能的实现类,所以也是提供了扩展解耦,允许开发者自己定义CallAdapterFactory和CallAdapter,去实现潜在的Call类型转换。

因为这种Call对象的生产需要有大量的配套代码,为了简化代码,Retrofit使用动态代理来生产这个对象。

最后,因为需要处理的方法和对象太多太复杂,需要使用建造者模式来把建造过程和使用过程分离开。

这样倒着走一遍之后,我们再看Retrofit的设计和实现原理,就会觉得水到渠成,对于Retrofit精妙的设计更会有一种切身体会。

借鉴与启示

在上文的反推过程中,我们可窥见(瞎猜)Jake大神的一些思路:

  1. 万物皆对象网络访问后,回调数据是个对象;网络访问本身也是个对象。
  2. 依赖倒置哪怕是使用自家的OkHttp,哪怕底层调用的始终是OkHttpClient,也需要依赖一个抽象的retrofit2.Call接口,依赖于抽象,而不是依赖于具体。
  3. 单一职责类的职责需要维持单一,流程需要但是超出自己职责的功能,去调用相关的类实现,比如OkHttpClient和ServiceMethod的各自职责与调用关系。
  4. 迪米特法则内部实现再复杂,对于外部调用者也只展示他需要的那些功能,例如Retrofit。
  5. 自动>人工动态代理的使用,可以用自动生成的模板代码,减轻人工编写配套代码的工作量,成本更低,风险更低。
  6. 利用工厂类开放扩展对于流程确定,但方法不能确定的,利用工厂类,对调用者开放扩展能力。
  7. 利用多个工厂类组成扩展列表如果1个工厂类不能实现兼得,何不设置一个工厂类列表,在多个工厂类中,看哪个工厂类能解决问题。
  8. 利用建造者模式把建造和使用分离这样使用者不需要关系复杂的建造过程,例如Retrofit和ServiceMethod。
  9. 利用外观模式减少对复杂子系统的操作虽然有复杂的子系统协同工作,调用者只需要调用最外层的Retrofit即可。
  10. 其他开放封闭、接口隔离、里式替换、静态代理等设计原则或设计模式都有体现也都很熟悉了,就不再啰嗦。

最后感叹一下。

对于网络访问的抽象与优化,实际上是个非常难的课题,在Retrofit之前,大家努力的方向基本上都是Volley/OkHttp这种围绕底层网络访问的工作。因为越底层的东西越容易抽象,越上升到接近业务层,就越容易在纷扰的业务层中迷失。Retrofit能精准地抓到Call网络工作对象这个关键点,并能通过一系列精巧的设计实现对这种类型“飘忽不定”的对象的自动化定制生产,着实令人赞叹

参考

Retrofit你真的会用Retrofit2吗?Retrofit2完全教程Retrofit2 源码解析Retrofit 框架源码学习拆轮子系列:拆 RetrofitAndroid 动态代理以及利用动态代理实现 ServiceHook

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-11-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我就是马云飞 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档