Android | 彻底理解 OkHttp 源码篇

1,639 阅读14分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

OkHttp 4.X 及其以上的版本的源码为 kotlin 语言编写。本文讲解的 OkHttp 源码版本为 3.14.9,是 Java 语言编写的最后一个版本。因为 OkHttp 使用了 Okio 这个 IO 库,所以阅读本文之前,我非常建议大家先阅读我之前写的两篇关于 Okio 的文章,再去阅读 OkHttp 的源码,就会扫清一道障碍。

  1. Android | 彻底理解 Okio 之源码篇
  2. Android | 彻底理解 Okio 超时机制

Http 请求/响应报文格式

okhttp 是一个 Http 协议的网络请求框架,专为 Java 和 Android 精心设计。所以我们有必要了解一些关于 Http协议的基础知识。Http 协议工作在 OSI 模型的应用层,基于TCP/IP通信协议来传递数据,使用 Socket 实现通信。

请求报文格式

Http 协议规定请求报文格式由请求行+请求头字段+请求体组成。

  • 请求行:由请求方法+URL+协议版本组成,如POST /api/listNotice HTTP/1.1
  • 请求头字段:键值对的形式组成,完整的头字段可以在 www.iana.org/assignments… 找到。
  • 请求体:请求发送的数据。

幻灯片1.PNG

响应报文格式

Http 协议规定响应报文格式由响应行+响应头字段+响应体组成。

  • 响应行:由协议版本+状态码+状态描述组成,如HTTP/1.1 200 ok
  • 响应头字段:键值对的形式组成,完整的头字段可以在 www.iana.org/assignments… 找到。
  • 响应体:响应返回的数据

幻灯片2.PNG

重要的头字段

头字段说明取值
Content-Type1. 在请求中 ,客户端告诉服务器实际发送的数据类型。2. 在响应中,Content-Type 标头告诉客户端实际返回的内容的内容类型www.iana.org/assignments…
Content-Length发送给接收方的消息主体的大小十进制整数,字节的数目
Connection当前的事务完成后,是否会关闭网络连接keep-alive 或 close
Cache-Control单向缓存指令,被用于在 http 请求和响应中,通过指定指令来实现缓存机制。developer.mozilla.org/zh-CN/docs/…
Authorization用于提供服务器验证用户代理身份的凭据,允许访问受保护的资源developer.mozilla.org/zh-CN/docs/…

回想一下 OkHttp 请求网络的过程

在正式开始分析源码之前,我们在脑海里回顾一下 OkHttp 发送一个网络请求要写哪些代码?对于一个简单的 Post 请求来说,要经历如下几个步骤。

  1. 使用 Builder 模式创建一个全局使用的OkHttpClient对象,可以用它来设置对所有请求都生效的超时时间、缓存、自定义拦截器等。
  2. 使用 Builder 模式创建一个Request,设置Request请求的 url,头字段,请求体等。
  3. 使用全局的OkHttpClient对象发送同步或异步请求,之后可以获取来自服务器的响应。

本文分析的源码,会以上述一个完整的网络请求流程为主线进行。

Headers 类的设计-头字段

OkHttp 中,Headers类表示 Http 请求或响应报文中的头字段,使用Builder模式来创建对象。

Headers 成员变量

private final String[] namesAndValues;

namesAndValues是一个字符串数组,用来存储头字段。因为头字段是键值对的形式,所以在namesAndValuesindex*2位置存储keyindex*2 + 1位置存储value。如下面的形式。

Headers 成员方法

下面是一些比较重要的成员方法介绍

// 根据 key(name) 获取头字段的 value
public @Nullable String get(String name)

// 返回头字段的个数
public int size()

// 返回在数组中第 index 个头字段的 key
public String name(int index)

// 返回在数组中第 index 个头字段的 value
public String value(int index)

// 返回整个请求头或响应头的字节数
public long byteCount()

RequestBody 类的设计-请求体

RequestBody是一个抽象类,内部没有成员变量。它表示 Http 请求报文中的请求体,在 Post 请求方式中,需要使用到RequestBody来构建请求体。根据请求体内容编码类型的不同,RequestBody有两个子类分别是FormBodyMultipartBody,对应application/x-www-form-urlencodedmultipart/form-data两种编码方式。

RequestBody 成员方法

// 返回媒体类型
public abstract @Nullable MediaType contentType()
// 写入数据到输出流
public abstract void writeTo(BufferedSink sink) throws IOException
// 返回请求体的字节数
public long contentLength() throws IOException
// 创建一个请求体
public static RequestBody create(params)

自定义媒体类型和输出流

RequestBody有两个抽象方法contentTypewriteTo,允许我们创建匿名类重写这两个方法自定义媒体类型(Content-Type)和写入到输出流的数据,更加灵活。

create 创建一个请求体

create有多个重载的方法,是RequestBody类创建RequestBody对象的默认实现。大多数时候,我们可以使用create方法来方便的创建一个请求体。

Request 类的设计-请求报文

我们已经熟悉了HeadersRequestBody,它们是Request类的重要组成部分。Request类表示一个请求报文,同样使用Builder模式来创建对象。

Request 成员变量

// 请求 url
final HttpUrl url;

// 请求方法(get, head, post, put, delete 等)
final String method;

// 请求头字段
final Headers headers;

// 请求体
final @Nullable RequestBody body;

// 浏览器缓存控制,存储了 Cache-Control 头字段相关的指令信息。
private volatile @Nullable CacheControl cacheControl; // Lazily initialized.

Request类的设计严格遵守了 Http 协议的请求报文格式,由请求行,请求头,请求体三大部分组成。同时Request类还对头字段Cache-Control的值做了封装。Request类的成员方法都是用来获取这些字段值的,这里不做介绍。

ResponseBody 类的设计-响应体

ResponseBody表示 Http 响应报文中的响应体,它被设计成一个抽象类,没有成员变量。它表示 Http 响应报文中的响应体。

ResponseBody 成员方法

// 返回媒体类型
public abstract @Nullable MediaType contentType();

// 返回响应体的字节数
public abstract long contentLength();

// 返回输入流,用于读取响应体的数据
public abstract BufferedSource source();

// 将响应体以字节数组的形式返回
public final byte[] bytes() throws IOException;

// 创建一个响应体
public static ResponseBody create(@Nullable MediaType contentType, String content);

实例化一个ResponseBody需要重写contentTypecontentLengthsource3个抽象方法。create有多个重载方法,是ResponseBody类创建ResponseBody实例的默认实现,我们可以使用create系列方法轻松创建出一个响应体。

Response 类的设计-响应报文

Response类表示响应报文,使用Builder模式来创建对象。

Response 成员变量

protocolcodemessage3个字段组成了响应行,headers字段表示响应头字段,body字段表示响应体。Response类成员方法都是用来获取字段值的,这里不做介绍。到此我们已经分析完了 OkHttp 中与请求报文和响应报文相关的类,了解这些基础类的设计有利于我们后面源码的阅读。

// 为响应 Http 重定向或身份验证生成的 request
final Request request;

// Http 协议版本
final Protocol protocol;

// Http 响应状态码
final int code;

// Http 状态描述
final String message;

// TLS 握手记录,Https 相关
final @Nullable Handshake handshake;

// 响应头字段
final Headers headers;

// 响应体
final @Nullable ResponseBody body;

// 从网络接收到的原始响应报文,若使用了缓存将会返回 null
final @Nullable Response networkResponse;

// 从缓存获取的原始响应报文,若没用使用缓存将会返回 null
final @Nullable Response cacheResponse;

// Http 重定向或身份验证的响应报文
final @Nullable Response priorResponse;

// 发送请求报文时的时间戳
final long sentRequestAtMillis;

// 收到响应时的时间戳
final long receivedResponseAtMillis;

// 单个 Http 请求和响应的管理类
final @Nullable Exchange exchange;

// 缓存控制,存储了 Cache-Control 头字段相关的指令信息。
private volatile @Nullable CacheControl cacheControl; // Lazily initialized.

Call 接口的设计-已经准备好的请求

Call是一个接口,它表示一个已经准备好被执行的请求(调用)。一个Call可以在执行过程中被取消,但是只能够执行一次,不允许被重复执行。

Call 接口方法

// 返回请求报文
Request request();

// 同步执行当前请求,会阻塞直到响应返回。返回后需要关闭资源,以防止资源泄露
Response execute() throws IOException;

// 异步执行当前请求
void enqueue(Callback responseCallback);

// 取消当前正在执行的请求
void cancel();

// 返回当前请求是否正在执行
boolean isExecuted();

// 返回当前请求是否被取消
boolean isCanceled();

// 返回超时对象
Timeout timeout();

// 复制一个一样的请求
Call clone();

RealCall 类的设计-Call 接口的实现类

Call是一个接口,RealCall类实现了这个接口。

RealCall 成员变量

// OkHttp 客户端
final OkHttpClient client;

// 应用层和网络层的数据传输媒介类
private Transmitter transmitter;

// 原始请求报文
final Request originalRequest;

// 是否为 WebSocket
final boolean forWebSocket;

// Guarded by this.
// 是否正在执行
private boolean executed;

RealCall 成员方法

RealCall除了实现Call接口中的方法外,还新增了newRealCallgetResponseWithInterceptorChain两个重要方法。下面会对重要的方法进行讲解。

// 静态方法,返回一个 RealCall 对象
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket);

// 返回被拦截器链处理过的响应报文
Response getResponseWithInterceptorChain() throws IOException;

newRealCall-实例化一个RealCall对象

newRealCall会对实例化一个RealCall对象,并对clienttransmitteroriginalRequestforWebSocket4个字段赋值初始化。

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
}

execute-同步执行当前请求

execute会开始同步执行当前的请求,并进行调用超时检测,即在 OkHttpClient 中设置的callTimeout调用超时包含的过程有:DNS解析,建立连接,写入请求报文,服务器处理,读取响应报文的全过程。如果在规定的超时时间内,这些过程没有完成,则认为是调用超时。

@Override public Response execute() throws IOException {
    synchronized (this) {	// 加同步锁,将 executed 置为 true
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }

    // 进入调用超时检测
    transmitter.timeoutEnter();
    // 请求开始事件回调
    transmitter.callStart();
    try {
      client.dispatcher().executed(this);
      // 同步执行本次请求,获取响应结果
      return getResponseWithInterceptorChain();
    } finally {
      // 结束本次请求
      client.dispatcher().finished(this);
    }
}

同步执行并不会开启一个线程去执行请求任务。如果你在主线程调用execute方法执行一个请求,那么主线程会一直阻塞等待直到结果返回。

enqueue-异步执行当前请求

enqueue是异步执行,请求会被线程池中的某个线程执行。该方法需要传入Callback类型的参数,响应结果将会在Callback接口的函数中回调,回调函数同样是在子线程中执行,所以不要在CallbackonFailureonResponse函数中对 UI 进行操作。

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {	// 加同步锁,将 executed 置为 true
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    // 请求开始事件回调
    transmitter.callStart();
    // 异步执行本次请求,
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

getResponseWithInterceptorChain- 获取经拦截器链处理过的响应报文

无论是同步请求还是异步请求,最终都会调用getResponseWithInterceptorChain方法来执行网络请求任务。这说明在 OkHttp 中任何一个请求都必须经过拦截器链的处理。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // 第一步添加用户自定义的应用拦截器
    interceptors.addAll(client.interceptors());
    // 第二步添加失败重试重定向拦截器
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    // 第三步添加桥接拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 第三部添加缓存拦截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // 第四步添加连接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
    // 第五步,若不是 WebSocket,则添加用户自定义的网络拦截器
      interceptors.addAll(client.networkInterceptors());
    }
    // 第六步,添加调用拦截器。这个拦截器链中的最后一个拦截器
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
            originalRequest, this, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      // 让拦截器链开始工作,返回响应结果
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
            closeQuietly(response);
            throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
            transmitter.noMoreExchanges(null);
      }
    }
}

可以清晰的看见,一个请求会先后经过自定义应用拦截器->失败重试重定向拦截器->桥接拦截器->缓存拦截器->连接拦截器->自定义网络拦截器->调用拦截器,最终发送到服务器。而响应结果经过的拦截器,刚好与上面的顺序相反,是自定义网络拦截器->连接拦截器->缓存拦截器->桥接拦截器->失败重试重定向拦截器->自定义应用拦截器。

Dispatcher 类的设计-异步请求的调度员

Dispatcher类设计了一个线程池,负责管理所有异步请求的执行,注意它并不负责同步请求的执行,在前面的execute同步请求方法中我们已经看见它直接执行了getResponseWithInterceptorChain方法来获取响应结果,并没有放在一个线程中去运行。

Dispatcher 成员变量

Dispatcher是个“调度员”,默认情况下最多可以同时执行 64 个异步请求任务,并且最多允许 5 个请求同时访问同一个主机。在一个异步或同步请求结果返回后,Dispatcher会检查还有没有请求在执行,若没有请求任务了,Dispatcher会去执行一个名为idleCallback的空闲任务,我们可以使用setIdleCallback方法来设置具体的空闲任务。同步请求和异步请求都使用双端队列保存。

// 默认最多可以同时执行 64 个请求
private int maxRequests = 64;

// 默认最多允许 5 个请求同时访问同一个主机
private int maxRequestsPerHost = 5;

// 没有请求任务时(所有请求执行完毕),Dispatcher 执行的空闲任务
private @Nullable Runnable idleCallback;

// 线程池
private @Nullable ExecutorService executorService;

// 准备被执行的异步请求
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

// 正在被执行的异步请求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

// 正在执行的同步请求
// 再次说明, Dispatcher 只负责保存和取消同步请求任务,并不会执行它
// 因为在调用 execute 方法后,同步任务就已经开始执行了
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

Dispatcher 成员方法

Dispatcher类可以用来执行保存在队列中的异步任务,并可以随时取消执行。

// 创建线程池
public synchronized ExecutorService executorService();

// 设置能同时执行的最大请求数
public void setMaxRequests(int maxRequests);

// 设置对每个主机的最大请求数
public void setMaxRequestsPerHost(int maxRequestsPerHost);

// 设置空闲任务
public synchronized void setIdleCallback(@Nullable Runnable idleCallback);

// 添加异步请求任务到队列中并准备执行
void enqueue(AsyncCall call);

// 在异步请求队列中查找是否已存在对目标主机 host 的请求
@Nullable private AsyncCall findExistingCallWithHost(String host);

// 取消所有请求的执行,包括同步和异步请求
public synchronized void cancelAll();

// 执行请求
private boolean promoteAndExecute();

// 添加同步请求任务到队列中
synchronized void executed(RealCall call);

// 请求任务完成
private <T> void finished(Deque<T> calls, T call);

// 返回当前等待被执行的请求
public synchronized List<Call> queuedCalls();

// 返回当前正在执行的请求
public synchronized List<Call> runningCalls();

promoteAndExecute-将异步任务放到线程池中执行

RealCallenqueue方法最终会调用到promoteAndExecute,这个方法会将readyAsyncCalls队列中的异步任务移动到runningAsyncCalls中,然后将这些任务放到executorService线程池中执行。

private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));

    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;

    // 将任务从 readyAsyncCalls 移动到 runningAsyncCalls
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
            AsyncCall asyncCall = i.next();

            // 若正在执行的异步任务数 >= maxRequests,退出循环
            if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
            // 若对当前主机的请求数 >= maxRequestsPerHost,刚请求本次不执行
            if (asyncCall.callsPerHost().get() >= maxRequestsPerHost,) continue; // Host max capacity.

            i.remove();
            asyncCall.callsPerHost().incrementAndGet();
            executableCalls.add(asyncCall);
            runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    // 将可执行的异步任务放到线程池中执行
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      // 调用 AsyncCall 的 executeOn 方法
      asyncCall.executeOn(executorService());
    }

    return isRunning;
}

AsyncCall 类的设计-执行异步请求的真正人

promoteAndExecute方法我们看到最后的异步请求任务会交给AsyncCallAsyncCallRealCall的一个内部类,它继承自NamedRunnable,而NamedRunnable实现了Runnable接口,所以AsyncCall可以被线程执行。AsyncCall中的execute方法正是被线程执行的方法。与RealCall同步请求的execute方法对比来说,整体流程基本一致。首先进入超时检测,接着启动拦截器链来处理请求,最后获取响应结果。

@Override protected void execute() {
  boolean signalledCallback = false;
  // 进入调用超时检测
  transmitter.timeoutEnter();
  try {
    // 经拦截器链处理后,获取响应结果
    Response response = getResponseWithInterceptorChain();
    signalledCallback = true;
    // 请求成功回调 onResponse 方法
    responseCallback.onResponse(RealCall.this, response);
  } catch (IOException e) {
    // 出现 IO 异常
    if (signalledCallback) {
      // Do not signal the callback twice!
      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      // 请求失败,回调 onFailure
      responseCallback.onFailure(RealCall.this, e);
    }
  } catch (Throwable t) {
    // 出现其他异常
    cancel();
    if (!signalledCallback) {
      IOException canceledException = new IOException("canceled due to " + t);
      canceledException.addSuppressed(t);
      // 请求失败,回调 onFailure
      responseCallback.onFailure(RealCall.this, canceledException);
    }
    throw t;
  } finally {
    // 告诉 Dispatcher 本次请求任务结束,你可以去执行其他待执行的任务或空闲任务
    client.dispatcher().finished(this);
  }
}

总结

本文介绍了 OkHttp 中与 Http 协议相关的类,异步请求任务的调度员Dispatcher,执行同步请求任务的RealCall,执行异步请求任务的AsyncCall。相信通过本文的分析,大家对 OkHttp 这个框架的工作流程有了基本的了解。本文所讲解的 OkHttp 源码侧重于一个网络请求的全过程,还有很多未涉及的地方,如拦截器、连接池、缓存等并没有深入分析他们的源码和应用,对于 OkHttp 更深一点的问题,后面我会写新的文章讲解,欢迎大家持续关注。

写在最后

如果你对我感兴趣,请移步到 blogss.cn ,或关注公众号:程序员小北,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注,这是我持续创作的动力 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
  • 本文首发于掘金,未经许可禁止转载 ©️