背景
Android中原生的网络请求方法
类型 | 优点 | 缺点 |
---|---|---|
HttpURLConnection | API简单,体积较小,因而非常适用于Android项目。压缩和缓存机制可以有效地减少网络访问的流量,在提升速度和省电方面也起到了较大的作用 | 2.2版本以下存在bug |
HttpClient | 实现比较稳定,bug数量也很少 | API数量过多、维护成本高、用法复杂 |
Volly | 处理小文件的http请求、 数据量不大 | 通信频繁的网络操作 大数据量的网络操作表现糟糕 |
OkHttp | 高效 支持SPDY、连接池、GZIP和HTTP缓存 自动处理常见的网络问题(重连、SSL握手) | |
Retrofit | 性能最好,处理最快、使用REST、API时非常方便、 传输层默认就使用OkHttp、支持NIO、拥有出色的API文档和社区支持 如果你的应用程序中集成了OKHttp,Retrofit默认会使用OKHttp处理其他网络层请求、默认使用Gson |
整体思路
从使用方法出发,首先是怎么使用,其次是我们使用的功能在内部是如何实现的,实现方案上有什么技巧,有什么范式。
首先放一张完整流程图
基本用例
GET A URL
1 | OkHttpClient client = new OkHttpClient(); |
POST TO A SERVER
1 | public static final MediaType JSON |
创建OkHttpClient对象
1 | OkHttpClient client = new OkHttpClient(); |
咦,怎么不见 builder?莫急,且看其构造函数:
1 | public OkHttpClient() { |
原来是方便我们使用,提供了一个“快捷操作”,全部使用了默认的配置。OkHttpClient.Builder 类成员很多,后面我们再慢慢分析,这里先暂时略过:
1 | public Builder() { |
发起HTTP请求
1 | String run(String url) throws IOException { |
OkHttpClient 实现了 Call.Factory,负责根据请求创建新的 Call.
那我们现在就来看看它是如何创建 Call 的:
1 | /** * Prepares the {@code request} to be executed at some point in the future. */ |
如此看来功劳全在 RealCall 类了,下面我们一边分析同步网络请求的过程,一边了解 RealCall 的具体内容。
同步网络请求
我们首先看 RealCall#execute:
1 |
|
这里我们做了 4 件事:
- 检查这个 call 是否已经被执行了,每个 call 只能被执行一次,如果想要一个完全一样的 call,可以利用 call#clone 方法进行克隆。
- 利用 client.dispatcher().executed(this) 来进行实际执行,dispatcher 是刚才看到的 OkHttpClient.Builder 的成员之一,它的文档说自己是异步 HTTP 请求的执行策略,现在看来,同步请求它也有掺和。
- 调用 getResponseWithInterceptorChain() 函数获取 HTTP 返回结果,从函数名可以看出,这一步还会进行一系列“拦截”操作。
- 最后还要通知 dispatcher 自己已经执行完毕。
dispatcher 这里我们不过度关注,在同步执行的流程中,涉及到 dispatcher 的内容只不过是告知它我们的执行状态,比如开始执行了(调用 executed),比如执行完毕了(调用 finished),在异步执行流程中它会有更多的参与。
真正发出网络请求,解析返回结果的,还是 getResponseWithInterceptorChain:
1 | private Response getResponseWithInterceptorChain() throws IOException { |
OkHttp 最核心的一个东西,不要误以为它只负责拦截请求进行一些额外的处理(例如 cookie),实际上它把实际的网络请求、缓存、透明压缩等功能都统一了起来,每一个功能都只是一个 Interceptor,它们再连接成一个 Interceptor.Chain,环环相扣,最终圆满完成一次网络请求。
从 getResponseWithInterceptorChain 函数我们可以看到,Interceptor.Chain 的分布依次是:
- 在配置 OkHttpClient 时设置的 interceptors;
- 负责失败重试以及重定向的 RetryAndFollowUpInterceptor;
- 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的 BridgeInterceptor;
- 负责读取缓存直接返回、更新缓存的 CacheInterceptor;
- 负责和服务器建立连接的 ConnectInterceptor;
- 配置 OkHttpClient 时设置的 networkInterceptors;
- 负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor。
在这里,位置决定了功能,最后一个 Interceptor 一定是负责和服务器实际通讯的,重定向、缓存等一定是在实际通讯之前的。
责任链模式在这个 Interceptor 链条中得到了很好的实践
对于把 Request 变成 Response 这件事来说,每个 Interceptor 都可能完成这件事,所以我们循着链条让每个 Interceptor 自行决定能否完成任务以及怎么完成任务(自力更生或者交给下一个 Interceptor)。这样一来,完成网络请求这件事就彻底从 RealCall 类中剥离了出来,简化了各自的责任和逻辑。两个字:优雅!
责任链模式在安卓系统中也有比较典型的实践,例如 view 系统对点击事件(TouchEvent)的处理.
回到 OkHttp,在这里我们先简单分析一下 ConnectInterceptor 和 CallServerInterceptor,看看 OkHttp 是怎么进行和服务器的实际通信的。
建立连接:ConnectInterceptor
1 | @Override |
实际上建立连接就是创建了一个 HttpCodec 对象,它将在后面的步骤中被使用,那它又是何方神圣呢?它是对 HTTP 协议操作的抽象,有两个实现:Http1Codec 和 Http2Codec,顾名思义,它们分别对应 HTTP/1.1 和 HTTP/2 版本的实现。
在 Http1Codec 中,它利用 Okio 对 Socket 的读写操作进行封装,Okio 以后有机会再进行分析,现在让我们对它们保持一个简单地认识:它对 java.io 和 java.nio 进行了封装,让我们更便捷高效的进行 IO 操作。
而创建 HttpCodec 对象的过程涉及到 StreamAllocation、RealConnection,代码较长,这里就不展开,这个过程概括来说,就是找到一个可用的 RealConnection,再利用 RealConnection 的输入输出(BufferedSource 和 BufferedSink)创建 HttpCodec 对象,供后续步骤使用。
发送和接收数据:CallServerInterceptor
1 | @Override |
- 向服务器发送 request header;
- 如果有 request body,就向服务器发送;
- 读取 response header,先构造一个 Response 对象;
- 如果有 response body,就在 3 的基础上加上 body 构造一个新的 Response 对象;
这里我们可以看到,核心工作都由 HttpCodec 对象完成,而 HttpCodec 实际上利用的是 Okio,而 Okio 实际上还是用的 Socket,所以没什么神秘的,只不过一层套一层,层数有点多。
其实 Interceptor 的设计也是一种分层的思想,每个 Interceptor 就是一层。为什么要套这么多层呢?分层的思想在 TCP/IP 协议中就体现得淋漓尽致,分层简化了每一层的逻辑,每层只需要关注自己的责任(单一原则思想也在此体现),而各层之间通过约定的接口/协议进行合作(面向接口编程思想),共同完成复杂的任务。
简单应该是我们的终极追求之一,尽管有时为了达成目标不得不复杂,但如果有另一种更简单的方式,我想应该没有人不愿意替换。
发起异步网络请求
1 | client.newCall(request).enqueue(new Callback() { |
这里我们就能看到 dispatcher 在异步执行时发挥的作用了,如果当前还能执行一个并发请求,那就立即执行,否则加入 readyAsyncCalls 队列,而正在执行的请求执行完毕之后,会调用 promoteCalls() 函数,来把 readyAsyncCalls 队列中的 AsyncCall “提升”为 runningAsyncCalls,并开始执行。
这里的 AsyncCall 是 RealCall 的一个内部类,它实现了 Runnable,所以可以被提交到 ExecutorService 上执行,而它在执行时会调用 getResponseWithInterceptorChain() 函数,并把结果通过 responseCallback 传递给上层使用者。
这样看来,同步请求和异步请求的原理是一样的,都是在 getResponseWithInterceptorChain() 函数中通过 Interceptor 链条来实现的网络请求逻辑,而异步则是通过 ExecutorService 实现。
返回数据的获取
在上述同步(Call#execute() 执行之后)或者异步(Callback#onResponse() 回调中)请求完成之后,我们就可以从 Response 对象中获取到响应数据了,包括 HTTP status code,status message,response header,response body 等。这里 body 部分最为特殊,因为服务器返回的数据可能非常大,所以必须通过数据流的方式来进行访问(当然也提供了诸如 string() 和 bytes() 这样的方法将流内的数据一次性读取完毕),而响应中其他部分则可以随意获取。
响应 body 被封装到 ResponseBody 类中,该类主要有两点需要注意:
- 每个 body 只能被消费一次,多次消费会抛出异常;
- body 必须被关闭,否则会发生资源泄漏;
在前面,我们看过body相关的代码:
1 | if (!forWebSocket || response.code() != 101) { |
由 HttpCodec#openResponseBody 提供具体 HTTP 协议版本的响应 body,而 HttpCodec 则是利用 Okio 实现具体的数据 IO 操作。
这里有一点值得一提,OkHttp 对响应的校验非常严格,HTTP status line 不能有任何杂乱的数据,否则就会抛出异常,
HTTP 缓存
在 同步网络请求 小节中,我们已经看到了 Interceptor 的布局,在建立连接、和服务器通讯之前,就是 CacheInterceptor,在建立连接之前,我们检查响应是否已经被缓存、缓存是否可用,如果是则直接返回缓存的数据,否则就进行后面的流程,并在返回之前,把网络的数据写入缓存。
这块代码比较多,但也很直观,主要涉及 HTTP 协议缓存细节的实现,而具体的缓存逻辑 OkHttp 内置封装了一个 Cache 类,它利用 DiskLruCache,用磁盘上的有限大小空间进行缓存,按照 LRU 算法进行缓存淘汰,这里也不再展开。
我们可以在构造 OkHttpClient 时设置 Cache 对象,在其构造函数中我们可以指定目录和缓存大小:
1 | public Cache(File directory, long maxSize); |
而如果我们对 OkHttp 内置的 Cache 类不满意,我们可以自行实现 InternalCache 接口,在构造 OkHttpClient 时进行设置,这样就可以使用我们自定义的缓存策略了。
总结
OkHttp 还有很多细节部分没有在本文展开,例如 HTTP2/HTTPS 的支持等,但建立一个清晰的概览非常重要。对整体有了清晰认识之后,细节部分如有需要,再单独深入将更加容易。
在文章最后我们再来回顾一下完整的流程图:
- OkHttpClient 实现 Call.Factory,负责为 Request 创建 Call;
- RealCall 为具体的 Call 实现,其 enqueue() 异步接口通过 Dispatcher 利用 ExecutorService 实现,而最终进行网络请求时和同步 execute() 接口一致,都是通过 getResponseWithInterceptorChain() 函数实现;
- getResponseWithInterceptorChain() 中利用 Interceptor 链条,分层实现缓存、透明压缩、网络 IO 等功能;