开始
又是新的一周,美好的一周,应该由源码解析来开始。很早就像看看OkHttp的源码,主要是对这个框架中的 Interceptor(拦截器) 比较感兴趣,废话少说,跟随源码的脚步来看看它的实现原理吧~
分析
OkHttp 的使用方法
看源码的第一步肯定要先会使用,那么OkHttp的一般使用方法很简单,代码如下:
1 |
|
更大部分网络请求框架一样,才用接口回调的方式来将结果进行返回并处理,但是需要注意的是在 onResponse 方法中是不能更新 UI 的。还有就是 onResponse 方法中的 response.body().string() 这个方法好像也有执行网络请求,因为如果在 runOnUiThread 的 run 方法中执行这行代码,会报 不能再 UI 线程中执行网络操作,具体原因我们后面慢慢分析。
Read the fucking source code
先分析一下OkHttpClinet这个类。这个类主要负责一个OkHttp的核心配置,很显然在上面我们并没设置任何配置,所以使用的都是默认的配置。然后调用的是 OkHttpClient 的 newCall 方法,这个方法直接 new 了一个 RealCall 返回,RealCall 是实现了 Call 接口的一个类。代码如下:
1 |
|
将 Request 对象传入 newCall() 方法,然后在调用 enqueue() 方法。enqueue 翻译过的意思是入列,这里可以推断出 OkHttp 肯定维护了一个或多个队列(废话)。代码如下:
1 |
|
可以看到,调用了 client 对象的 dispatcher 的 enqueue(),并且 new 了一个 AsyncCall 对象传入。有异步就肯定有同步,对应的方法是 execute() 方法,代码如下:
1 |
|
同步和异步
我们先分析一下 enqueue() 方法,从上面的源码中可以看出,最终会调用 dispatcher 的 enqueue 方法。代码如下:
1 |
|
在 enqueue 方法中首先会判断当前正在执行的请求是否小于最大请求数量,然后在判断当前正在执行的请求的主机和入列请求的主机相同的个数是否小于最大主机数(maxRequestsPerHost),如果没有超出限制就将 AsyncCall 添加到 runningAsyncCalls 中,然后使用 executorService() 执行 AsyncCall。这里的 executorService 是一个线程池,execute() 方法表示从线程池中取出一个线程并执行。我们来看看线程执行了哪些代码。
1 |
|
短短20行代码,可以说是 OkHttp 的核心代码,所以我们先看大致流程,首先会通过 getResponseWithInterceptorChain() 方法获取 Response 对象,然后判断请求是否被取消,如果取消就调用 onFailure 回调。如果没有没有取消就调用 onResponse 方法。
上面我们有个问题就是在 onResponse 方法中直接更新UI 会报不能在线程中更新UI的错误。原因就在这里,这里的onResponse 方法是在子线程中被调用的,所以是不能更新UI的。
最后在 catch 中调用了 onFialure 和在 finally 中调用了 dispatcher的finished方法。enqueue() 方法的执行流程大概就上面几个步骤。OkHttp除了可以异步请求获取数据以外,还可以通过获取数据,调用 RealCall 的 execute方法,代码如下:
1 |
|
代码逻辑和 AsyncCall 中的 execute 方法差不多,不过要注意的是这里是同步的,所以如果直接在 Activity 中调用 execute 方法会报错。在上面 execute() 方法的代码中,无论是同步还是异步都在最后调用了 finish 方法,我们来看看这个方法。
1 |
|
dispatcher 中一共有三个 finished() 方法,前两个 finished() 方法调用了第三个 finished() 方法。第一个 finsihed 方法是由异步的 Call 调用的,第二个 finished() 方法是由同步的 Call 调用到的。在第三个 finished() 方法中会把 Call 对象从对应的 队列中移除.
如果当 promoteCalls 为 true 的时候会调用 promoteCalls() 方法。在这个方法中会把 readyAsyncCalls 队列中的 Call 添加到 runningAsyncCalls 中并执行。
ok,异步和同步的大致步骤差不多就上面这些,其中主要逻辑大同小异,在执行异步Call的时候,会在执行完之后把等待的 Call 全部添加到执行的队列中并执行。
一共有三个队列。
- runningAsyncCalls (异步执行队列,每个 enqueue 的 Call 都会添加这个队列中)
- runningSyncCalls (同步执行队列,同步任务会被添加到这个队列中)
- readyAsyncCalls (异步等待队列,当 runningAsyncCalls 达到最大限制的时候,后续 enqueue 的 Call 会被添加到此队列中)
拦截器
上面仅仅只分析了大体的流程,真正执行网络请求的地方并没有看到,很显然OkHttp把最复杂的地方巧妙的隐藏起来了。重新回到 execute() 方法中的 getResponseWithInterceptorChain() 方法,代码如下:
1 |
|
Interceptor 中文意识为拦截器,可以看到这个方法中有一个拦截器集合,其中系统提供了几个拦截器,每个拦截器都有自己的作用。
最后将拦截器集合传入 RealInterceptorChain 中并调用了 proceed() 方法。代码如下:
1 |
|
首先是一些异常处理,重点在 21 行处,又创建了一个 RealInterceptorChanin 对象,并把这个对象传入 interceptor 的 intercept() 方法中。
第一个被调用 intercept() 方法的拦截器是 RetryAndFollowUpInterceptor(前提是没有自定义拦截器,如果有自定义拦截器,OkHttp 先调用我们自定义的拦截的 intercept() 方法)。我们来看看 intercept() 方法的具体实现。代码如下:
1 |
|
代码实在有点多,我们只看重点的一行,就是在 intercept() 方法内部会调用 RealInterceptorChain 的 proceed() 方法,这个时候我们看上面的 proceed() 方法,在 new RealInterceptorChain 的时候把 index + 1 了,这个时候就会调用下一个拦截器的 intercept() 方法,代码如下:
1 | //BridgeInterceptor.java |
what? 这个代码不是很眼熟吗?可以看看后面的几个拦截器的 intercept 方法,在方法中都调用了 chain.proceed,有在 proceed 方法中调用下一个拦截器的 intercept 方法,如果此循环下去 就会把所有拦截器的 intercept 都调用一遍,这里有点像方法的递归调用,既然是递归,那肯定是会有一个”终点”,不然这个方法就成了一个死循环,那么这里的”终点”就在最后一个拦截器的 intercept 方法中,类名叫做 CallServerInterceptor,在这类中有这样第一段注释 :
This is the last interceptor in the chain. It makes a network call to the server.
意识就是说,这个拦截器是链中最后一个拦截器,它对服务器进行了网络调用。
到这里,整个拦截器的调用链就走完了,这里会一层一层的往上返回,调用逻辑大概是这样的:
请求:
RealInterceptorChain -> 自定义拦截器 -> RetryAndFollowUpInterceptor -> BridgeInterceptor -> CacheInterceptor -> ConnectInterceptor -> CallServerInterceptor
响应:
CallServerInterceptor -> ConnectInterceptor -> CacheInterceptor -> BridgeInterceptor -> RetryAndFollowUpInterceptor -> 自定义拦截器 -> RealInterceptorChain
重点逻辑在各个拦截器中的 intercept() 方法中,我们可以这么理解在 intercept() 方法中 调用 proceed() 方法之前的代码是用来处理请求的,之后的代码是用来处理响应的。伪代码入下:
1 |
|
Request 和 Response
上面我们分析了拦截器的调用流程,可以发现这整个流程离不开两个对象,Request 和 Response,下面来看看这两个类的具体实现.代码如下:
1 |
|
代码虽然有点长,但是很简单,采用一个标准的构建者模式
设计。构建者模式
的好处就是可以省略复杂的构造函数,使用 Builder 去构建对象,需要设置什么参数就调用什么方法,最后使用 build() 方法去构建真正的对象。
在 url() 方法中对 url 进行判断,如果使用的是 WebSocket URL,那么就替换成 HTTP URL。然后解析 URL 并返回一个 HttpUrl 对象,很显然这个 HttpUrl 就是对 URL 字符串的一个封装。至于 HttpUrl 中的 parse() 方法就是对一个 URL 字符串进行解析,涉及到 HTTP 协议的相关知识,这里不作深研。
在 cacheControl 属性后面有一段注释: Lazily initialized. 意思就是这个属性是懒加载的,在看下面的 cacheControl() 方法,所以 Request 的cache 只有在调用 cacheControl() 方法才会被负赋值。
下面来看看 Response 类,代码如下:
1 |
|
因为代码太过恐怖,就不深入研究了,但是在 string 方法上有这样一段注释:
This method loads entire response body into memory. If the response body is very large this may trigger an {@link OutOfMemoryError}. Prefer to stream the response body if this is a possibility for your response.
意识大概是,string 这个方法会把服务器返回的 响应体(ResponseBody) 加载到内存中,如果返回的数据过大的话,就会导致抛出 OutOfMemoryError,所以如果服务器返回的数据过大的情况下推荐使用 inputStream 方法获取流对象来操作。
上面说到 string 方法会把请求体的数据加载到内存中,这是一个网络操作,所以这个方法是不能在 UI 线程中调用的,这也是本文一开始例子中为什么要把更新 UI 和 string 方法分开写的原因。
自定义拦截器
上面我们分析了拦截器的实现原理,下面我们来试试怎么自定义拦截器。
1 |
|
在上面的分析中,只要不是最后一个拦截器,就必须调用 proceed() 方法来递归调用其余的拦截器,上面我们就调用了 proceed() 方法,这样看好像觉得拦截器没什么卵用,但是其实拦截器在平时开发中有很大的作用,比如我们有个需求必须要在每个请求添加一个 token 的请求头,如果我们不知道拦截器,可能需要在每个请求的地方都加上一句 (伪代码) header.add(token),但是当我们知道拦截器之后,就可以使用拦截器来实现这一需求。代码如下:
1 |
|
Cache
缓存对一个网络请求框架很重要,一个有缓存的系统和一个没有缓存的系统有这天壤之别,特别是在请求量大的情况下。所谓的缓存就是将请求返回的信息存储在本地,然后判断这个缓存是否过期,如果没有过期就使用缓存,如果过期了就重新从服务器获取并把最新的数据缓存在本地。这样可以很有效的降低服务器的压力。
在上篇博客中,分析 Volley 的源码的时候,在其内部有一个专门处理缓存的线程,而且所有的请求都是先走缓存线程,然后在走网络线程的。具体可以看我上一篇博客。
在上面分析拦截器的时候有看到一个类名叫做 CacheInterceptor 的拦截器,从名字上分析这个拦截器是用来处理缓存的,我们来看看内部的具体实现。
1 |
|
上面代码中,最核心的地方就是 CacheStrategy.Factory.get() 方法,这个方法可以理解为 通过请求(Request)对象,和 缓存响应(cacheCandidate)对象,生成缓存策略,下面的逻辑的都是根据缓存策略来执行的,来看看其内部的逻辑。代码如下:
1 |
|
通过 getCandidate() 方法获取 CacheStrategy 对象,这个方法后面分析,接下来会判断,如果获取到的缓存Response 的 networRequest 不为空,且 这个请求只从缓存中获取即 CacheControl 的 onlyIfCached 为 true,这个时候看之前 CacheInteceptor 的 inteceptor 方法里面的逻辑,会直接返回一个 code 为 504 的 response 对象。
1 |
|
其中有代码的意思都写在注释上了,还有有些细节的地方需要说明一下。
缓存的寿命 = receivedAge(接收寿命,此字段不太清楚含义) + responseDuration(响应持续时间) + residentDuration (缓存存货时间)。具体逻辑在 cacheResponseAge() 方法中。
在类介绍中说到,CacheStrategy 类通过 Request 和 Cache Response 对象来决定是使用 网络还是缓存,还是两者都使用。换句话说,这里所谓的缓存策略是通过 request 和 cacheResponse 两个对象共同决定的,比不是说有缓存,就用缓存,还需要 request 对象瞒住条件才行。
CacheStrategy 中的 networkRequest 如果为 null 即表示 request 不使用网络,cacheResponse 为 null 即表示此次调用不使用缓存。比如本地缓存获取不到的时候 cacheResponse 就会为 null,缓存过期 cacheResponse 就会为 null,等等。
当 cacheResponse 的总存活时间小于总可存活事件,即表示 当前请求还是缓存时间内,可以直接使用本地缓存。所以这个时候 networkRequest = null,在 CacheInterceptor 的 intercept() 方法中,会判断如果 networkRequest == null就直接使用本地缓存,这也对应了 HTTP 协议里面的缓存策略。
1 |
|
我们在回到 CacheInterceptor 的 intercept() 方法中,里面的判断就很好理解了,如果 cacheResponse == null 就表明不要使用缓存而是而是重新请求,这个时候会把 cacheCandidate 的流关闭。如果 networkRequest == null 且 cacheResponse == null 即表示 网络不可用,所以返回 504,如果 networkRequest == null 即表示缓存是可用的 不需要重新请求服务器。然后就是调用 proceed 获取 response 对象,如果 cacheResponse != null 且 response code == 304(not_modified) 即表示 此处详情没有更新可以直接使用本地缓存,所以就直接把 cacheResponse 返回,在返回之前会对本地的缓存进行更新操作,最后如果 cache 不为null(cache 是在 OkHttp 对象中设置的),且响应有body,响应可以被缓存,那么就会将 response 缓存在本地,如果不满足上面条件,就会判断的当前请求的 method 是否是 get 如果不是 get 就会把本地的缓存移除。自此OKHttp 的 缓存实现就分析完毕。
总结
最后贴一张 OkHttp 拦截器的工作原理图
OKHttp 比系统提供的 HttpUrlConnection 更好的 API 和 设计,个人认为拦截器这种设计方法非常巧妙,将一个复杂的 Http 请求分割成几个部分,分别交由不同的拦截器处理。而且我们还可以自己定义自己的拦截器,来处理很多复杂的业务逻辑。拦截器这种设计思在别的地方也有很多实现,特别是在网络框架上用的非常广泛,可能因为是 TCP/IP 协议本身就是基于拦截器思想设计的吧~(以上纯属个人看法)。
上面说到的拦截器思想指的是,整个调用是通过不同类递归调用,下一个拦截器的 Request 是基于上一个拦截器的,上一个拦截器的 Response 是基于下一个拦截器返回的,我们可以在里面手动修改 HTTP 的信息。