Android OkHttp使用与分析

安卓开发领域,很多重要的问题都有了很好的开源解决方案,例如网络请求 OkHttp + Retrofit 简直就是不二之选。“我们不重复造轮子不表示我们不需要知道轮子该怎么造及如何更好的造!”,在用了这些好轮子将近两年之后,现在是时候拆开轮子一探究竟了。本文基于 OkHttp 截至 2016.7.11 的最新源码对其进行了详细分析。

1,整体思路

从使用方法出发,首先是怎么使用,其次是我们使用的功能在内部是如何实现的,实现方案上有什么技巧,有什么范式。全文基本上是对 OkHttp 源码的一个分析与导读,非常建议大家下载 OkHttp 源码之后,跟着本文,过一遍源码。对于技巧和范式,由于目前我的功力还不到位,分析内容没多少,欢迎大家和我一起讨论。
首先放一张完整流程图(看不懂没关系,慢慢往后看):


okhttp_full_process
okhttp_full_process

2,基本用例

来自 OkHttp 官方网站

2.1,创建 OkHttpClient 对象

OkHttpClient client = new OkHttpClient();

咦,怎么不见 builder?莫急,且看其构造函数:

public OkHttpClient() {

 this(new Builder());
}

原来是方便我们使用,提供了一个“快捷操作”,全部使用了默认的配置。OkHttpClient.Builder
类成员很多,后面我们再慢慢分析,这里先暂时略过:

public Builder() {
    dispatcher = new Dispatcher();
    protocols = DEFAULT_PROTOCOLS;
    connectionSpecs = DEFAULT_CONNECTION_SPECS;
    proxySelector = ProxySelector.getDefault();
    cookieJar = CookieJar.NO_COOKIES;
    socketFactory = SocketFactory.getDefault();
    hostnameVerifier = OkHostnameVerifier.INSTANCE;
    certificatePinner = CertificatePinner.DEFAULT;
    proxyAuthenticator = Authenticator.NONE;
    authenticator = Authenticator.NONE;
    connectionPool = new ConnectionPool();
    dns = Dns.SYSTEM;
    followSslRedirects = true;
    followRedirects = true ;
    retryOnConnectionFailure = true;
    connectTimeout = 10_000;
    readTimeout = 10_000;
    writeTimeout = 10_000;
}

2.2,发起 HTTP 请求

String run(String url) throws IOException {
    Request request = new Request.Builder()
        .url(url)
        .build();
    Response response = client.newCall(request).execute();
    return  response.body().string();
}

OkHttpClient 实现了 Call.Factory,负责根据请求创建新的 Call,在 拆轮子系列:拆 Retrofit中我们曾和它发生过一次短暂的遭遇:callFactory
负责创建 HTTP 请求,HTTP 请求被抽象为了 okhttp3.Call
类,它表示一个已经准备好,可以随时执行的 HTTP 请求
那我们现在就来看看它是如何创建 Call 的:


/**
* Prepares the {@code request} to be executed at some point in the future.
*/

@Override
public Call newCall(Request request) {
    return new RealCall( this , request);
}

如此看来功劳全在 RealCall类了,下面我们一边分析同步网络请求的过程,一边了解 RealCall的具体内容。

2.2.1,同步网络请求

我们首先看 RealCall#execute:

@Override
public Response execute() throws IOException {
    synchronized(this) {
        if (executed)  throw
            new IllegalStateException( "Already Executed" ); 
// (1)
      executed = true;
    }
    try{
        client.dispatcher().executed(this);                                
// (2)
        Response result = getResponseWithInterceptorChain();               
// (3)
        if(result == null) throw
            new IOException("Canceled");
        return result;
    }finally{
        client.dispatcher().finished(this);                                
// (4)
    }
}

这里我们做了 4 件事:
检查这个 call 是否已经被执行了,每个 call 只能被执行一次,如果想要一个完全一样的 call,可以利用 call#clone
方法进行克隆。
利用 client.dispatcher().executed(this)
来进行实际执行,dispatcher
是刚才看到的OkHttpClient.Builder
的成员之一,它的文档说自己是异步 HTTP 请求的执行策略,现在看来,同步请求它也有掺和。
调用 getResponseWithInterceptorChain()
函数获取 HTTP 返回结果,从函数名可以看出,这一步还会进行一系列“拦截”操作。
最后还要通知 dispatcher
自己已经执行完毕。

dispatcher 这里我们不过度关注,在同步执行的流程中,涉及到 dispatcher 的内容只不过是告知它我们的执行状态,比如开始执行了(调用 executed
),比如执行完毕了(调用 finished
),在异步执行流程中它会有更多的参与。
真正发出网络请求,解析返回结果的,还是 getResponseWithInterceptorChain:

private Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<interceptor> interceptors = new ArrayList<>();

    interceptors.addAll(client.interceptors());

    interceptors.add(retryAndFollowUpInterceptor);

    interceptors.add( new BridgeInterceptor(client.cookieJar()));

    interceptors.add( new CacheInterceptor(client.internalCache()));

    interceptors.add( new ConnectInterceptor(client));

    if(!retryAndFollowUpInterceptor.isForWebSocket()) {
        interceptors.addAll(client.networkInterceptors());
}

interceptors.add(new CallServerInterceptor(retryAndFollowUpInterceptor.isForWebSocket()));

Interceptor.Chain chain = new RealInterceptorChain(interceptors,null,null,null,0, originalRequest);

return chain.proceed(originalRequest);

}

OkHttp 开发者之一介绍 OkHttp 的文章里面,作者讲到:
the whole thing is just a stack of built-in interceptors.

可见 Interceptor
是 OkHttp 最核心的一个东西,不要误以为它只负责拦截请求进行一些额外的处理(例如 cookie),实际上它把实际的网络请求、缓存、透明压缩等功能都统一了起来,每一个功能都只是一个 Interceptor,它们再连接成一个 Interceptor.Chain,环环相扣,最终圆满完成一次网络请求。从 getResponseWithInterceptorChain函数我们可以看到,Interceptor.Chain的分布依次是:


okhttp_interceptors
okhttp_interceptors

在配置 OkHttpClient
时设置的 interceptors;
负责失败重试以及重定向的 RetryAndFollowUpInterceptor;
负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的 BridgeInterceptor;
负责读取缓存直接返回、更新缓存的 CacheInterceptor;
负责和服务器建立连接的 ConnectInterceptor;
配置 OkHttpClient时设置的 networkInterceptors;
负责向服务器发送请求数据、从服务器读取响应数的 CallServerInterceptor。

在这里,位置决定了功能,最后一个 Interceptor 一定是负责和服务器实际通讯的,重定向、缓存等一定是在实际通讯之前的。
责任链模式在这个 Interceptor
链条中得到了很好的实践(感谢 Stay 一语道破,自愧弗如)。
它包含了一些命令对象和一系列的处理对象,每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。

对于把 Request
变成 Response
这件事来说,每个 Interceptor
都可能完成这件事,所以我们循着链条让每个 Interceptor
自行决定能否完成任务以及怎么完成任务(自力更生或者交给下一个Interceptor
)。这样一来,完成网络请求这件事就彻底从 RealCall
类中剥离了出来,简化了各自的责任和逻辑。两个字:优雅!
责任链模式在安卓系统中也有比较典型的实践,例如 view 系统对点击事件(TouchEvent)的处理,具体可以参考Android设计模式源码解析之责任链模式中相关的分析。
回到 OkHttp,在这里我们先简单分析一下 ConnectInterceptor
和 CallServerInterceptor
,看看 OkHttp 是怎么进行和服务器的实际通信的。

2.2.1.1,建立连接:ConnectInterceptor

@Override
public Response intercept(Chain chain) throws IOException {

    RealInterceptorChain realChain = (RealInterceptorChain) chain;

    Request request = realChain.request();

    StreamAllocation streamAllocation = realChain.streamAllocation();

// We need the network to satisfy this request. Possibly for validating a conditional GET.

    boolean doExtensiveHealthChecks = !request.method().equals(
"GET"
);

  
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);

  
RealConnection connection = streamAllocation.connection();

 
  
return realChain.proceed(request, streamAllocation, httpCodec, connection);

}

实际上建立连接就是创建了一个 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对象,供后续步骤使用。

2.2.1.2,发送和接收数据:CallServerInterceptor

@Override
public Response intercept(Chain chain) throws IOException {

    HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();

    Request request = chain.request();

    long sentRequestMillis = System.currentTimeMillis();

    httpCodec.writeRequestHeaders(request);

    if(HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

        request.body().writeTo(bufferedRequestBody);

        bufferedRequestBody.close();

    }

httpCodec.finishRequest();

Response response = httpCodec.readResponseHeaders()
    .request(request)
    .handshake(streamAllocation.connection().handshake())
    .sentRequestAtMillis(sentRequestMillis)
    .receivedResponseAtMillis(System.currentTimeMillis())
    .build();
 if(!forWebSocket || response.code() != 101) {
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
    }
if("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) {

    streamAllocation.noNewStreams();
}

 
  
// 省略部分检查代码

return response;

}

我们抓住主干部分:
向服务器发送 request header;
如果有 request body,就向服务器发送;
读取 response header,先构造一个 Response对象;
如果有 response body,就在 3 的基础上加上 body 构造一个新的 Response
对象;

这里我们可以看到,核心工作都由 HttpCodec对象完成,而 HttpCodec实际上利用的是 Okio,而 Okio 实际上还是用的 Socket,所以没什么神秘的,只不过一层套一层,层数有点多。
其实 Interceptor的设计也是一种分层的思想,每个 Interceptor
就是一层。为什么要套这么多层呢?分层的思想在 TCP/IP 协议中就体现得淋漓尽致,分层简化了每一层的逻辑,每层只需要关注自己的责任(单一原则思想也在此体现),而各层之间通过约定的接口/协议进行合作(面向接口编程思想),共同完成复杂的任务。
简单应该是我们的终极追求之一,尽管有时为了达成目标不得不复杂,但如果有另一种更简单的方式,我想应该没有人不愿意替换。

2.2.2,发起异步网络请求

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        System.out.println(response.body().string());
    }
});

 
// RealCall#enqueue
    @Override
    public void enqueue(Callback responseCallback) {
        synchronized(this ) {
            if(executed) throw
                new IllegalStateException( "Already Executed" );
                executed = true ;
        }

        client.dispatcher().enqueue( new AsyncCall(responseCallback))
    }

 
// Dispatcher#enqueue

synchronized void enqueue(AsyncCall call) {
    if(runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {

        runningAsyncCalls.add(call);

        executorService().execute(call);
    }else{
        readyAsyncCalls.add(call);
    }
}

这里我们就能看到 dispatcher 在异步执行时发挥的作用了,如果当前还能执行一个并发请求,那就立即执行,否则加入 readyAsyncCalls 队列,而正在执行的请求执行完毕之后,会调用promoteCalls()函数,来把 readyAsyncCalls队列中的 AsyncCall “提升”为runningAsyncCalls,并开始执行。这里的 AsyncCall 是 RealCall 的一个内部类,它实现了 Runnable,所以可以被提交到ExecutorService上执行,而它在执行时会调用getResponseWithInterceptorChain() 函数,并把结果通过 responseCallback 传递给上层使用者。
这样看来,同步请求和异步请求的原理是一样的,都是在 getResponseWithInterceptorChain()函数中通过 Interceptor链条来实现的网络请求逻辑,而异步则是通过 ExecutorService 实现。

2.3,返回数据的获取

在上述同步(Call#execute()执行之后)或者异步(Callback#onResponse()回调中)请求完成之后,我们就可以从 Response对象中获取到响应数据了,包括 HTTP status code,status message,response header,response body 等。这里 body 部分最为特殊,因为服务器返回的数据可能非常大,所以必须通过数据流的方式来进行访问(当然也提供了诸如 string()和 bytes()这样的方法将流内的数据一次性读取完毕),而响应中其他部分则可以随意获取。响应 body 被封装到 ResponseBody类中,该类主要有两点需要注意:每个 body 只能被消费一次,多次消费会抛出异常;body 必须被关闭,否则会发生资源泄漏;

2.2.1.2,发送和接收数据:CallServerInterceptor 小节中,我们就看过了 body 相关的代码:

if(!forWebSocket || response.code() != 101) {
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
}

由 HttpCodec#openResponseBody提供具体 HTTP 协议版本的响应 body,而 HttpCodec则是利用 Okio 实现具体的数据 IO 操作。这里有一点值得一提,OkHttp 对响应的校验非常严格,HTTP status line 不能有任何杂乱的数据,否则就会抛出异常,在我们公司项目的实践中,由于服务器的问题,偶尔 status line 会有额外数据,而服务端的问题也毫无头绪,导致我们不得不忍痛继续使用HttpUrlConnection,而后者在一些系统上又存在各种其他的问题,例如魅族系统发送 multi-part form 的时候就会出现没有响应的问题。

2.4,HTTP 缓存

2.2.1,同步网络请求 小节中,我们已经看到了 Interceptor
的布局,在建立连接、和服务器通讯之前,就是 CacheInterceptor
,在建立连接之前,我们检查响应是否已经被缓存、缓存是否可用,如果是则直接返回缓存的数据,否则就进行后面的流程,并在返回之前,把网络的数据写入缓存。
这块代码比较多,但也很直观,主要涉及 HTTP 协议缓存细节的实现,而具体的缓存逻辑 OkHttp 内置封装了一个 Cache类,它利用 DiskLruCache,用磁盘上的有限大小空间进行缓存,按照 LRU 算法进行缓存淘汰,这里也不再展开。
我们可以在构造 OkHttpClient时设置 Cache 对象,在其构造函数中我们可以指定目录和缓存大小:

public Cache(File directory, long maxSize);

而如果我们对 OkHttp 内置的 Cache类不满意,我们可以自行实现 InternalCache接口,在构造OkHttpClient时进行设置,这样就可以使用我们自定义的缓存策略了。

3,总结

OkHttp 还有很多细节部分没有在本文展开,例如 HTTP2/HTTPS 的支持等,但建立一个清晰的概览非常重要。对整体有了清晰认识之后,细节部分如有需要,再单独深入将更加容易。
在文章最后我们再来回顾一下完整的流程图:


okhttp_full_process
okhttp_full_process

OkHttpClient 实现 Call.Factory,负责为 Request 创建 Call;
RealCall 为具体的 Call 实现,其 enqueue() 异步接口通过 Dispatcher 利用ExecutorService 实现,而最终进行网络请求时和同步 execute() 接口一致,都是通getResponseWithInterceptorChain() 函数实现;getResponseWithInterceptorChain() 中利用 Interceptor 链条,分层实现缓存、透明压缩、网络 IO 等功能;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容