OkHttp作为android非常流行的网络框架,笔者认为有必要剖析此框架实现原理,抽取并理解此框架优秀的设计模式。OkHttp有几个重要的作用,如桥接、缓存、连接复用等,本文笔者将从使用出发,解读源码,剖析此功能的实现原理。最后阅读完源码后总结出如下结论,OkHttp是一款优秀的网络请求框架,内部采用优雅的责任链模式、构造模式、桥接模式、享元模式、门面模式等设计模式,符合依赖导致原则、里氏替换原则等面向对象原则,将复杂的网络请求封装从简单的调用,属实优雅。笔者推荐感兴趣的读者从笔者粗陋的源码解读思路去思考更多的源码设计与实现,彻底完全了解OkHttp的设计思路,并抽象出时序图和类图。
以下是笔者粗陋的时序图总结,供读者参考。
如上图,我们从OkHttp提供的API出发。
OkHttpClient,从名字可知,网络请求的客户端,该客户端主要的作用是什么呢?不妨通过其Builder的重要成员进行分析,
dispatch分发器,主要负责网络请求队列的调度,稍后再谈。
connectionPool,连接池。
interceptors,拦截器-责任链,OkHttp内置了几个重要的拦截器,有失败-重定向、桥接、缓存、连接、访问服务等。
剩下的成员读者可自行分析。那么OkHttpCient笔者认为是一系列网络请求的基础配置。
Call是一个接口,继承了克隆接口,我们看下定义的方法,
request,返回Request,
execute,同步执行此处请求,
enqueue,异步执行此次请求,
cancel,取消此次请求,
isExecuted,是否执行完毕,
isCanceld,是否成功取消,
timeout,返回超时相关
因此,Call可以理解为一次Request,通过OkHttpClient.newCall创建,其唯一实现是RealCall,
构造函数中需传入OkHttpClient,原始请求Request,是否WebSocket属性。
Request不用笔者多说,主要封装request,包裹了url、method、headers、body等http基础request字段。
封装了message,code,hearer,body等。
以上Request和Response是网络请求的主要实体类。
笔者认为这是异步请求时主要的调度器,调度对象是Call,内部定义有如下三种队列,
raedyAsyncCalls是通过Call#enqueue添加到此的Call队列,
runningAsyncCalls,从readyAsyncCalls中转移到此队列,表现正在运行的异步Call,
runningSyncCalls,同步运行队列,
下面我们看下调度方法promoteAndExecute
主要在enqueue、finished时调用,作用是将符合条件的raedyAsyncCalls转移至runningAsyncCalls中。
在从readyAsyncCalls转移到runningAsyncCalls时,有默认最大运行数maxRequests64。将真正执行运行的Call放进executeableCalls,然后碟调调用其executeOn方法,传入的executorService是一个线程池,
核心线程数定义0,最大容量MAX_VALUE,因maxRequests的存在,可以忽略此参数。
Interceptor只定义了一个方法Intercept,参数为Chain,每个拦截器负责处理自己的逻辑,如果处理完毕并且需要下一个拦截器处理,需要显式调用Chain#proceed方法。
我们看下Chain接口的唯一实现RealInterceptorChain,
传入此次请求call,拦截器,拦截器执行下标index0,原始请求request等,主要供多个拦截器从Chain中获取信息,我们看下核心方法process,
Interceptor每调用一次proceed方法,会触发Chain被负责,传入index+1(这使得同一个拦截器可以多次调用proceed方法,从该节点重试),进一步获取到下一个拦截器,再调用其拦截器intercept方法。这样就实现了链式请求。
以上,我们介绍了OkHttp框架的主要角色,下面介绍下一次请求的主要流程,以及各个重要拦截器所做的工作。
笔者还得从创建了一个Call对象开始,
我们跟进enqueue方法
原子式设置executed为true,否则抛出异常,合理,一个call只能调用enqueue一次。
client是通过RealCall构造方法传入,我们进入Dispatcher#enqueue查看。笔者注意,这里的RealCall在进入dispatcher时,被转换成了AsyncCall,这个稍后再谈。
所做的事情,是将异步call放入readAsyncCalls中,如果不是webSocket需做点什么,这个笔者不展开说,主要调用到调度方法promoteAndExecute。
这个方法如在Dispatch中解读,加入runningAsyncCalls,并且加入到executableCalls列表,调用AsyncCall#executeOn方法,我们跟进。
AsyncCall实现了Running接口,并且封装了RealCall,我们直接看run方法的实现,
通过TimeOut#enter开启请求超时调度(如果设置的话),然后最重要的调用了getResponseWithInterceptorChain方法,直接返回请求到的Response。当请求完毕后,最终调用到dispatcher.finished方法,我们暂且先查看finished方法,
将执行完毕的Call从runingAsyncCalls中移除,然后在调用一次promoteAndExecute方法,将准备队列的Call执行,如果没有Call执行了,就调用闲置回调idleCallback。这样就实现了队列的简单调度。那么,我们将注意力重新回到核心方法getResponseWithInterceptorChain。
创建一个拦截器list,先放入OkHttpClient中用户自定义的拦截器,随后放入几个核心拦截器,
RetryAndFollowUpInterceptor、负责重试重定向的拦截器。
BridgeInterceptor、桥接拦截器,负责自动设置一些heads、cook等。
CacheInterceptor、缓存的核心实现拦截器。
ConnectInterceptor、连接拦截器,维护了一个连接池,复用连接核心逻辑拦截器。
CallServerInterceptor、与服务器正式请求的拦截器,
这些全部封装进RealInterceptorChain方法中,然后调用proceed方法,参数是request,
通过上文我们了解proceed是顺序调用下一个拦截器逻辑,因此,笔者这里暂忽略用户自定义拦截器,直接顺序解读核心拦截器实现逻辑。
一个无限循环,直接调用chain.process让下一个拦截器处理,然后解析Response,
通过followUpRequest解析Response,如果返回空,代表无需重试或重定向,直接返回Response。否则,重复调用chain#proceed(注意,chain在realChanin中通过copy方法实现原型模式,因此后面的index+1对此处无影响,chain#index仍为原始值)
我们看下followUpRequest方法,
对各种Response#code作解析,新创建Request,当Response正常返回,此方法返回null,笔者在此处不展开说,有兴趣的读者可自行研究。接下来,我们看下一个拦截器。
此处,将OkHttpClient的cookieJar保存,继续跟进拦截逻辑,
(1)如果存在body,body中存在contentType,自动设置进Request的Hearer中,
(2)如果存在body,且内容长度不等于-1,自动设置Content-Length头,移除Transfer-Encoding头。否则,移除Content-Length头,添加Transfer-Encoding头。感兴趣的读者可以主动去了解下这些请求头的意思。
(3)如果Request的Hearer中Host为空,则从请求url中设置host。
(4)如果没有设置Connection字段,自动设置“Keep-Alive”,意保持连接。
(5)继续添加请求头,Cookie、User-Aagent,
笔者在这里解释,桥接拦截器的作用就是自动设置一些请求头,减少客户端操作复杂度。
接下来,就是对Response作解析,如cookieJar解析、Response#解码相关,感兴趣的读者自行了解,笔者在此不展开讲。
实现缓存相关,核心逻辑如下,
根据Request从cache中获取Response,随后获取Request的缓存策略
如果从缓存中获取到Response,但是cacheResponse为null,代表此次请求不适用缓存,调用closeQuitely关闭缓存。
如果不能使用网络,且无缓存,返回失败Response。
如果不访问网络请求,那就直接从缓存中获取并返回,就不走接下来的拦截器了。
如果可以访问网络,但策略是访问网络,调用listenr#cacheConditionlHit回调,通知观察者缓存命中或缓存没命中。
于是,请求到下一个拦截器,当返回Response时,
Response code 返货 not modified,调用cache方法更新缓存
如果Response有效,添加到缓存中,另外笔者注意到,如果method不支持缓存,则移除,我们看下哪些不支持呢?
POST/DELETE/PATCH/PUT/MOVE是不支持缓存的,而GET/HEAD...才支持缓存。
通过获取到exchange,调用realChain#copy方法将exchange传入,作为一次连接复用。我们跟进看看initExchange方法,
通过exchangeFinder#find方法复用ExchangeCodec,笔者猜测这是实现连接的主要核心类,我们现看下ExchangeCodec是什么。ExchangeCodec是一个接口,方法定义如下,
从方法中,笔者猜测到这代表了连接实体,通过flushRequest发送给服务器请求,我们看看这个接口的实现类
笔者这里看下Http1ExchangeCodec,有兴趣的读者可以自己去看Http2ExchangeCodec,
从成员中发现RealConnection,因此这封装了一次连接;
BufferedSink,从命名知这是连接打开的缓冲区,通过flushReques将缓存区的数据发送给服务端。具体如何发送给服务器笔者暂不跟进。
先回到复用逻辑,exchangeFinder#find方法,
(1),首先从连接池中获取连接,如果无法获取,则新建连接,
调用RealCall#acquireConnectionNoEvents方法,将复用逻辑设置进connect成员中。
(2)如果没有复用连接,则新创建连接,
我们跟进connect,笔者直接快进到connectSockt,
当连接成功后,获取到source和sink,前者作为接受Response的缓存,后者就是发送缓存,
关于如何发送,上文已经介绍。那么如何接收呢?如下,在Http1ExchangeCodec#AbstractSource中。
跟进到底层有如下代码,笔者在此不再跟进,后面是基本的一些读取相关。有兴趣读者自行了解。
在这个拦截器中,笔者注意到创建或者复用了Connect,接下来,就是通过连接访问服务器了。
通过exchange写入request中的Hearer到缓存区中,
写入请求体什么的,到缓存区中。
最后,调用exchange,finishRequest,通过sink,flush,发送数据并清空缓存区。
随后,通过readResponseHearer,获取Response请求头,
获取请求头完毕后,接下来去读取请求body,
随后赋值给Response,即完成了一次请求。通过将Response链式返回给Chain,最后在AsyncCall中调用onResponse方法,即可通知客户端请求完成。
于是乎,一次请求的过程解读完毕,感谢读者的耐心观看。