拆轮子笔记 - OkHttp

作者: Goo_Yao | 来源:发表于2017-01-10 17:15 被阅读654次

前言

用了那么久的OkHttp,解决了不少的联网问题。对于热门的好轮子,总不能一直停留在会用这个层面上吧,是时候动手拆拆轮子,学习一下其中的原理。本文主要记录笔者通过各种网络学习资源及OkHttp源码的过程,希望通过自身学习研究的过程,给其他同学提供一些参考与帮助,如有不足,恳请指教。

  • 本文记录基于 OkHttp 3.4.1 源码的学习分析过程。
  • 笔者水平有限,内容可能基于学习资源,当然也会有个人的一些见解加入其中,仅作个人笔记用途,同时也试图探索学习如何入手拆轮子的好方法推荐给各位,如有侵权,马上删除。

学习资源

学习记录

跟随Piasy 拆轮子(学习资源 - 第一篇)

阅读心得:

  1. 从最实际的基本使用方法进行拓展、步步深入分析
  2. 没有过多深入到细节进行解析,进阶知识需要继续钻研
  3. 是带领大家入手拆轮子的好文章。(强烈建议各位边阅读边看源码,更加有助于理解其中的实现方式!)
  4. 再次感谢 Piasy 大神

文章知识点:

  1. 关注 OkHttp 整体工作流程,结合源码解析了“创建 OkHttpClient 对象”、“发起 HTTP 请求”、“同步网络请求”、“异步网络请求”等使用方法
  2. 详解了其中应用的核心设计模式:责任链模式
  3. 分析了 OkHttp 如何“建立连接”、“发送和接收数据”、“发起异步网络请求”、“获取返回数据”、“Http缓存”

学习笔记:

这里贴出笔者阅读文章时,看源码的顺序与结合理解的注解。

  1. 从 OkHttp 创建对象使用方法入手
    OkHttpClient client = new OkHttpClient();
  2. 进入构造方法,发现内部会创建 Builder
//构造方法中已初始化 Builder
public OkHttpClient() {
  this(new Builder());
}
  1. 建造者模式?进入 OkHttpClient.Builder 构造方法一探究竟,直接创建的 OkHttpClient 会默认使用基本配置。
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;
}
  1. 接下来,看看发起 Http 请求 OkHttp 用法。
String run(String url) throws IOException {
  //构造请求体
  Request request = new Request.Builder()
      .url(url)
      .build();    
  //发起请求核心代码
  Response response = client.newCall(request).execute();
  return response.body().string();
}
  1. 方法解析:client.newCall(request) - 根据请求创建新的 Call 类
@Override public Call newCall(Request request) {
  //实际构造并返回 RealCall 对象
  return new RealCall(this, request);
}
  1. 方法解析:client.newCall(request).execute() - 执行请求
@Override 
public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

上述代码主要做了4件事:

  1. 判断是否被执行 - 说明每个call只能被执行一次;另:可通过clone方法得到一个完全一样的Call(该方法是 Object类的方法)
  2. 利用client.dispatcher().executed(this)
 //dispatcher()方法返回 dispatcher ,异步http请求策略(内部使用 ExecutorService 实现)
 public Dispatcher dispatcher() {
    return dispatcher;
  }
  1. 调用 getResponseWithInterceptorChain() 获取Http返回结果(InterceptorChain - 拦截链? 一系列拦截操作待分析)
//方法解析:构建一个完整的 interceptors List,最后利用该 list 构建 Interceptor.Chain
private Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //1.添加 client 携带的所有 interceptors (配置 OkHttpClient 时候用户设置的 interceptors )
    interceptors.addAll(client.interceptors());
    //2.添加 retryAndFollowUpInterceptor (负责失败重试以及重定性)
    interceptors.add(retryAndFollowUpInterceptor);
    //3.添加由 client.cookieJar() 构建的 BridgeInterceptor(负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的相应转换为用户友好响应 - 即客户端与服务器端沟通的桥梁)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //4.添加由 client.internalCache() 构建的 CacheInterceptor (负责读取缓存直接返回、更新缓存)
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //5.添加由 client 构建的 ConnectInterceptor (负责与服务器建立连接)
    interceptors.add(new ConnectInterceptor(client));
    //如果 forWebSocket 则添加 client 携带的所有 networkInterceptors(配置OkHttpClient 时候用户设置的 networkInterceptors)
    if (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    //添加 CallServerInterceptor (负责向服务器发送给请求数据、从服务器读取响应数据)
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));
    //构建 Interceptor.Chain ,最后调用 chain.proceed(originalRequest),第7点有解析
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }
  - **“责任链模式”科普(百度百科)**:在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
  - **“责任链模式”科普(维基百科)**:它包含了一些命令对象和一系列的处理对象,每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。
  - **总结**:拦截链中,每个Interceptor都可处理 Request,返回 Response。运行时,顺着拦截链,让每个Interceptor 自行决定是否处理以及怎么处理(不处理则交给下一个Interceptor ),这样,可将处理网络请求从 RealCall 类中剥离,简化了各自责任与逻辑
  - **另**:责任链模式 在 Android 有着许多典型应用,例:view的点击事件分发(Android源码设计模式一书中有提及)
  1. dispatcher 如果try{}没有抛出异常,并且 result != null(则不执行return,下面的finally才执行),最后还会通知 dispatcher 操作完成
  2. 所以拦截链是如何工作的? 方法解析 - chain.proceed(originalRequest)
public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream,
      Connection connection) throws IOException {
    //首先需要各种判错
    if (index >= interceptors.size()) throw new AssertionError();
    calls++;
    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpStream != null && !sameConnection(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }
    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpStream != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }
    //然后再调用拦截链中的拦截器,最终得到 response
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpStream, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    //保证拦截链调用逻辑无误
    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpStream != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }
    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }
    //返回 response
    return response;
  }
  1. 明白拦截链的整体工作流程后,那么 OkHttp 又如何与服务器进行实际通信的呢?这里需要分析 ConnectInterceptor 拦截器。
//负责与目标服务器连接、将请求传递给下一个拦截器
/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;
  public ConnectInterceptor(OkHttpClient client) {
    this.client = client; 
  }
  //核心方法
  @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");
    //创建 HttpStream(接口) 对象这里的实现类是 Http2xStream、Http1xStream 分别对应 HTTP/1.1 和 HTTP2 版本
    //两者源码有点长,需要交给读者们自行深究,其中使用了 Okio 对 Socket 读写操作进行封装
    //Okio 可暂时认为是对 java.io、java.nio 进行封装,提供更高效的IO操作
    HttpStream httpStream = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpStream, connection);
  }
}

另外,创建 HttpStream 对象还涉及 StreamAllocation、RealConnection 对象。
由于篇幅过长,这里不贴出源码,给出总体创建思路:找到可用的RealConnection,再利用 RealConnection 的输入输出(BufferedSource、BufferedSink)创建 HttpStream 对象。

  1. 接下来,来弄懂 OkHttp 如何发送、接收数据,需要分析拦截链中最后一个拦截器 CallServerInterceptor
//拦截链中最后一个拦截器,负责向服务器发送给请求数据、从服务器读取响应数据
/** This is the last interceptor in the chain. It makes a network call to the server. */
public final class CallServerInterceptor implements Interceptor {
  private final boolean forWebSocket;
  public CallServerInterceptor(boolean forWebSocket) {
    this.forWebSocket = forWebSocket;
  }
  @Override public Response intercept(Chain chain) throws IOException {
    HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
    Request request = chain.request();
    long sentRequestMillis = System.currentTimeMillis();
    //1、写入要发送的 Http Request Headers
    httpStream.writeRequestHeaders(request);
    //2、如果请求方法允许,且 request.body 不为空,就加上一个body
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      //得到一个能传输 request body 的output stream
       Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
      //利用 Okio 将 requestBodyOut 写入,得到 bufferedRequestBody
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      //将 request body 写入到 bufferedRequestBody
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    }
    //刷新 request 到 socket
    httpStream.finishRequest();
    //构造新的 Response 对象
    Response response = httpStream.readResponseHeaders()
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
    if (!forWebSocket || response.code() != 101) {
      response = response.newBuilder()
          .body(httpStream.openResponseBody(response))
          .build();
    }
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }
    int code = response.code();
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;
  }
}
  • 核心工作基本由 HttpStream 完成(旧版本该类原名:HttpCodec ),利用了 Okio ,而 Okio 实际上还是使用了 Socket。
  • 分析(来源于Piasy 拆OkHttp):InterceptorChain 设计是一种分层思想,每层只关注自己的责任(单一责任原则),各层间通过约定的接口/协议进行合作,共同完成负责任务
  1. 初步学习了同步请求后,再从 OkHttp 异步网络请求用法中入手 OkHttp 异步网络请求的原理吧
//核心方法 - enqueue
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());
    }
});
  1. 进一步研究 RealCall.enqueue 方法
//源码 - RealCall.enqueue
@Override 
public void enqueue(Callback responseCallback) {
  //同步锁,如果已经执行会抛出异常
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  //关键调用 - Dispatcher.enqueue
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
  1. 继续深入,方法解析 - Dispatcher.enqueue
//概述:同步方法,如果当前还能执行一个并发请求,则加入 runningAsyncCalls ,立即执行,否则加入 readyAsyncCalls 队列
synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}
  • 方法中涉及 AsyncCall 类 - RealCall 的一个内部类,它实现了 Runnable,因此可以提交到 ExecutorService 上执行
  • 它在执行时会调用 getResponseWithInterceptorChain() 函数,并把结果通过 responseCallback 传递给上层使用者
  • 总结:同步请求跟异步请求的原理基本一致,最后都是调用 getResponseWithInterceptorChain() 函数,利用拦截链来实现的网络请求逻辑,只是实现方式不同,异步请求需要通过 ExecutorService 来调用getResponseWithInterceptorChain。
  1. 原来同步、异步请求有着异曲同工之妙,探究完 OkHttp 请求发送,当然要继续探究下返回数据的获取啦。
  • 完成同步或是异步的请求后,我们就可以从 Response 对象中获取到相应数据了,而其中值得注意的,也是最重要的,便是 body 部分了,因为一般服务器返回的数据较大,必须通过数据流的方式来访问。
  • 响应 body 被封装到 ResponseBody 类中,需要注意两点:
    • 每个 body 只能被消费一次,多次消费会出现异常
    • body 必须被关闭,否则会资源泄漏
  1. 最后再来看看 Http 缓存,需要探究 CacheInterceptor 拦截器
//在 ConnectInterceptor 之前添加的一个拦截器,也就是说,在建立连接之前需要看看是否有可用缓存,如果可以则直接返回缓存,否则就继续建立网络连接等操作
//代码较长、这里贴出核心部分(OkHttp 缓存处理逻辑)
@Override 
public Response intercept(Chain chain) throws IOException {
   ...
    //无可用缓存,放弃
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
    //有可用缓存,但被强制要求联网,那交给下个拦截器,继续联网了
    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }
    //不需要联网,返回缓存
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      //不管成不成功,都要记得关闭 cache body,避免内存泄漏
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
    //如果有缓存响应,就进行相应的获取
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (validate(cacheResponse, networkResponse)) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
        //更新缓存
        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    ...
    return response;
  }
  1. 关于 OkHttp 内部的缓存实际实现?
  • 实现方式:主要涉及 HTTP 协议缓存细节的实现,而具体的缓存逻辑 OkHttp 内置封装了一个 Cache 类,它利用 DiskLruCache,用磁盘上的有限大小空间进行缓存,按照 LRU 算法进行缓存淘汰。(源码略长,需要各位自行查看钻研)
  • InternalCache(接口),我们可以实现该接口,使用我们自定义的缓存策略

知识总结 - For 跟随Piasy 拆轮子

最后,再回头看看 Piasy 画的流程图,将知识串起来

Piasy - OkHttp 整体流程图
  • 核心方法:getResponseWithInterceptorChain - 拦截链模式(《Android 源码设计模式》 一书中有讲解),层层分明,单一责任
  • 同步、异步请求差异?(异步通过提交到 ExecutorService 来实现,最终还是离不开 getResponseWithInterceptorChain 方法)
  • 其中的提及到的重点拦截器:
  • retryAndFollowUpInterceptor(负责失败重试以及重定向)
  • BridgeInterceptor(负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的相应转换为用户友好响应)
  • CacheInterceptor(负责读取缓存直接返回、更新缓存)
  • ConnectInterceptor(负责与服务器建立连接)
  • CallServerInterceptor(负责向服务器发送给请求数据、从服务器读取响应数据)

相关文章

网友评论

  • 生椰拿铁锤:mark 流程图可以画的更好的哦
  • 57a4a9eb5580:这么好的文章为何没有人点赞呢。
    Goo_Yao::joy: 哈哈,过奖过奖,主要是记录一个学习过程 + 一些个人体会~

本文标题:拆轮子笔记 - OkHttp

本文链接:https://www.haomeiwen.com/subject/sqbkbttx.html