OkHttp源码解析,面试总结

// try again to read the actual response

responseBuilder = httpCodec.readResponseHeaders(false);

response = responseBuilder

.request(request)

.handshake(streamAllocation.connection().handshake())

.sentRequestAtMillis(sentRequestMillis)

.receivedResponseAtMillis(System.currentTimeMillis())

.build();

code = response.code();

}

realChain.eventListener()

.responseHeadersEnd(realChain.call(), response);

if (forWebSocket && code == 101) {

// Connection is upgrading, but we need to ensure interceptors see a non-null response body.

response = response.newBuilder()

.body(Util.EMPTY_RESPONSE)

.build();

} else {

response = response.newBuilder()

.body(httpCodec.openResponseBody(response))

.build();

}

if (“close”.equalsIgnoreCase(response.request().header(“Connection”))

|| “close”.equalsIgnoreCase(response.header(“Connection”))) {

streamAllocation.noNewStreams();

}

if ((code == 204 || code == 205) && response.body().contentLength() > 0) {

throw new ProtocolException(

"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());

}

return response;

}

}

CallServerInterceptor 负责向服务器发起真正的访问请求,并在接收到服务器返回后读取响应返回。

整体流程

整个网络访问的核心步骤,总结起来如下图所示:

OkHttp interceptor

任务队列

OkHttp 的任务队列在内部维护了一个线程池用于执行具体的网络请求。而线程池最大的好处在于通过线程复用减少非核心任务的损耗。

线程池的优点

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处

理器单元的闲置时间,增加处理器单元的吞吐能力。但如果对多线程应用不

当,会增加对单个任务的处理时间。可以举一个简单的例子:

假设在一台服务器完成一项任务的时间为 T ,

T1 创建线程的时间 ;

T2 在线程中执行任务的时间,包括线程间同步所需时间 ;

T3 线程销毁的时间 ;

显然 T = T1+T2+T3。注意这是一个极度简化的假设。

可以看出 T1、T3 是多线程本身带来的开销(在 Java 中,通过映射 pThead, 并进一步通过SystemCall 实现 native 线程),我们渴望减少 T1、T3 所用的时间,从而减少 T 的时间。但一些线程的使用者并没有注意到这一点,所以在程序中频繁的创建或销毁线程,这导致 T1 和 T3 在 T 中占有相当比例。

线程池技术正是关注如何缩短或调整 T1,T3 时间的技术,从而提高服务器程序性能的。

  • 通过对线程进行缓存,减少了创建销毁的时间损失

  • 通过控制线程数量阀值,减少了当线程过少时带来的 CPU 闲置(比如说长时间卡在I/O 上了)与线程过多时对 JVM 的内存与线程切换时系统调用的压力。

类似的还有 Socket 连接池、DB 连接池、CommonPool(比如 Jedis)等技术。

OkHttp 的任务队列主要由两部分组成:

  • 任务分发器 dispatcher:负责为任务找到合适的执行线程

  • 网络请求任务线程池

public final class Dispatcher {

private int maxRequests = 64;

private int maxRequestsPerHost = 5;

private @Nullable Runnable idleCallback;

/** Executes calls. Created lazily. */

private @Nullable ExecutorService executorService;

/** Ready async calls in the order they’ll be run. */

private final Deque readyAsyncCalls = new ArrayDeque<>();

/** Running asynchronous calls. Includes canceled calls that haven’t finished yet. */

private final Deque runningAsyncCalls = new ArrayDeque<>();

/** Running synchronous calls. Includes canceled calls that haven’t finished yet. */

private final Deque runningSyncCalls = new ArrayDeque<>();

public Dispatcher(ExecutorService executorService) {

this.executorService = executorService;

}

public Dispatcher() {

}

public synchronized ExecutorService executorService() {

if (executorService == null) {

executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,

new SynchronousQueue(), Util.threadFactory(“OkHttp Dispatcher”, false));

}

return executorService;

}

}

参数说明如下:

  • readyAsyncCalls:待执行异步任务队列

  • runningAsyncCalls:运行中异步任务队列

  • runningSyncCalls:运行中同步任务队列

  • executorService:任务队列线程池:

public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue workQueue,

ThreadFactory threadFactory) {

this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,

threadFactory, defaultHandler);

}

}

  • int corePoolSize: 最小并发线程数,这里并发同时包括空闲与活动的线

程,如果是 0 的话,空闲一段时间后所有线程将全部被销毁

  • int maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理

  • long keepAliveTime: 当线程数大于 corePoolSize 时,多余的空闲线程的最大存活时间,类似于 HTTP 中的 Keep-alive

  • TimeUnit unit: 时间单位,一般用秒

  • BlockingQueue workQueue: 工作队列,先进先出

  • ThreadFactory threadFactory: 单个线程的工厂,可以打 Log,设置 Daemon(即当 JVM 退出时,线程自动结束)等

在 OkHttp 中,构建了一个阀值为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活 60 秒,它使用了一个不存储元素的阻塞工作队列,一个叫做"OkHttp Dispatcher"的线程工厂。

也就是说,在实际运行中,当收到 10 个并发请求时,线程池会创建十个线 程,当工作完成后,线程池会在 60s 后相继关闭所有线程。

Dispatcher 分发器

dispatcher 分发器类似于 Ngnix 中的反向代理,通过 Dispatcher 将任务分发到合适的空闲线程,实现非阻塞,高可用,高并发连接。

dispatcher

同步请求

当我们使用 OkHttp 进行同步请求时,一般构造如下:

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder() .url(“https://www.wanandroid.com//hotkey/json”)

.build();

Response response = client.newCall(request).execute();

@Override public Response execute() throws IOException {

try {

client.dispatcher().executed(this);

Response result = getResponseWithInterceptorChain();

if (result == null) throw new IOException(“Canceled”);

return result;

} catch (IOException e) {

eventListener.callFailed(this, e);

throw e;

} finally {

client.dispatcher().finished(this);

}

}

同步请求的执行逻辑是:

  • 将对应任务加入分发器

  • 执行任务

  • 执行完成后通知 dispatcher 对应任务已完成,对应任务出队

异步请求

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder() .url(“https://www.wanandroid.com//hotkey/json”)

.build();

client.newCall(request).enqueue(new Callback() {

@Override public void onFailure(Call call,IOException e) {

Log.d(“OkHttp”,“Call Failed:”+e.getMessage());

}

@Override public void onResponse(Call call,Response response) throws IOException {

Log.d(“OkHttp”,“Call succeeded:”+response.message());

}

});

当 OkHttpClient 的请求入队时,根据代码,我们可以发现实际上是Dispatcher 进行了入队操作。

Dispatcher.java:129

synchronized void enqueue(AsyncCall call) {

if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {

//添加正在运行的请求

runningAsyncCalls.add(call);

//线程池执行请求

executorService().execute(call);

} else {

//添加到缓存队列排队等待

readyAsyncCalls.add(call);

}

}

如果满足条件:

  • 当前请求数小于最大请求数(64)

  • 对单一 host 的请求小于阈值(5)

将该任务插入正在执行任务队列,并执行对应任务。如果不满足则将其放入待执行队列。

接下来看看 AsyncCall的execute方法

@Override protected void execute() {

boolean signalledCallback = false;

try {

//执行I/O耗时任务

Response response = getResponseWithInterceptorChain();

if (retryAndFollowUpInterceptor.isCanceled()) {

signalledCallback = true;

//回调,这里回调是在线程池中

responseCallback.onFailure(RealCall.this, new IOException(“Canceled”));

} else {

signalledCallback = true;

//回调,这里回调是在线程池中

responseCallback.onResponse(RealCall.this, response);

}

} catch (IOException e) {

if (signalledCallback) {

// Do not signal the callback twice!

Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);

} else {

eventListener.callFailed(RealCall.this, e);

responseCallback.onFailure(RealCall.this, e);

}

} finally {

//通知分发器相关任务 已结束

client.dispatcher().finished(this);

}

}

Dispatcher.java:198

private void finished(Deque calls, T call, boolean promoteCalls) {

int runningCallsCount;

Runnable idleCallback;

synchronized (this) {

if (!calls.remove(call)) throw new AssertionError(“Call wasn’t in-flight!”);

if (promoteCalls) promoteCalls();

runningCallsCount = runningCallsCount();

idleCallback = this.idleCallback;

}

if (runningCallsCount == 0 && idleCallback != null) {

idleCallback.run();

}

}

  • 空闲出多余线程,调用 promoteCalls 调用待执行的任务

  • 如果当前整个线程池都空闲下来,执行空闲通知回调线程(idleCallback)

private void promoteCalls() {

if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.

if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {

AsyncCall call = i.next();

if (runningCallsForHost(call) < maxRequestsPerHost) {

i.remove();

runningAsyncCalls.add(call);

executorService().execute(call);

}

if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.

}

}

promoteCalls 的逻辑也很简单:扫描待执行任务队列,将任务放入正在执行任务队列,并执行该任务

总结

以上就是整个任务队列的实现细节,总结起来有以下几个特点:

  • OkHttp 采用 Dispatcher 技术,类似于 Nginx,与线程池配合实现了高并发,低阻塞的运行

  • Okhttp 采用 Deque 作为缓存,按照入队的顺序先进先出

  • OkHttp 最出彩的地方就是在 try/finally 中调用了 finished 函数,可以主动控制等待队列的移动,而不是采用锁或者 wait/notify,极大减少了编码复杂性

缓存策略
HTTP 协议中缓存部分的相关域

合理地利用本地缓存可以有效地减少网络开销,减少响应延迟。HTTP 报头也定义了很多与缓存有关的域来控制缓存。

首先来了解下 HTTP 协议中缓存部分的相关域。

Expires

超时时间,一般用在服务器的 response 报头中用于告知客户端对应资源的过期时间。当客户端需要再次请求相同资源时先比较其过期时间,如果尚未超过过期时间则直接返回缓存结果,如果已经超过则重新请求。

Cache-Control

相对值,单位时秒,表示当前资源的有效期。Cache-Control 比 Expires 优先级更高:

Last-Modified-Date

客户端第一次请求时,服务器返回:

Last-Modified: Tue, 12 Jan 2021 09:31:27 GMT

当客户端二次请求时,可以头部加上如下 header:

If-Modified-Since: Tue, 12 Jan 2021 09:31:27 GMT

如果当前资源没有被二次修改,服务器返回 304 告知客户端直接复用本地缓存。

ETag

ETag 是对资源文件的一种摘要,可以通过 ETag 值来判断文件是否有修改。当客户端第一次

请求某资源时,服务器返回:

ETag: “5694c7ef-24dc”

客户端再次请求时ÿ

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值