OkHttp学习笔记

OkHttp浅析

最近在看HTTP相关内容时,突然想到OkHttp是支持HTTP2.0的,所以特地看了下OkHttp的源码,并以此记录。

OkHttp VS Volley

这两个库都是项目用的比较多的

volley与okhttp对比图

OkHttp有什么优势?

  1. 支持HTTP2/SPDY
  2. socket自动选择最好的路线,并支持自动重连
  3. 拥有自动维护的socket连接池,减少握手次数
  4. 拥有队列线程池,轻松写并发
  5. 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)
  6. 基于Headers的缓存策略

OkHttp能竞争过其他库的核心功能是什么?

okhttp使用Dispatcher进行线程分发,它有两种方法,一个是普通的同步单线程;另一种是使用了队列进行并发任务的分发(Dispatch)与回调,这也是okhttp能够竞争过其它库的核心功能之一

OkHttp请求流程是什么样的?

总体分为两种:一种同步请求,一种异步请求

大体流程如下:

工作流程

同步请求

同步请求

异步请求

异步请求

OkHttp中Intercept是什么?

Intecept基于责任链模式实现,是一个监视、重写、重试请求的强有力机制。拦截器可以串联。

拦截器分为两类:

  • 应用拦截器,在发送请求之前和获取响应之后进行操作的。通过addInterceptor来进行添加。
  • 网络拦截器,在进行网络获取前进行操作的。通过addNetworkInterceptor进行添加。

拦截器执行顺序:

应用拦截器和网络拦截器各有什么优势?

应用拦截器

  • 不需要考虑中间状态的响应,比如重定向或者重试
  • 应用拦截器只会调用一次,即便数据来源于缓存
  • 只考虑应用的初始意图,不考虑OkHttp注入的Header,比如:if-None-Match,意思就是不管其他外在因素,只考虑最终的返回结果
  • 自定义的应用拦截器是第一个开始的执行的拦截器,所以可以决定是否执行其他的拦截器,通过Chain.proceed()方法。
  • 允许重试和发送多条请求,调用Chain.proceed()方法

网络拦截器

  • 网络拦截器可以操作重定向和失败重连的返回值
  • 我们可以看出,取缓存中的数据不会去进行网络请求,也就不会执行Chain.proceed()。
  • 意思是通过网络拦截器可以观察到所有通过网络传输的数据
  • 请求服务连接的拦截器先于网络拦截器执行,在进行网络拦截器的时候,可以看到Request中服务器的请求连接信息。

OkHttp是怎么复用连接池的?

OkHttp支持5个并发的KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)

StreamAllocation是计数对象,通过引用计数来管理socket,专门用于淘汰末位的Socket,淘汰策略如下:

  1. 并发socket空闲连接数超过5个
  2. 某个socket的keepalive的时间大于五分钟

Connection自动回收是怎么实现的?

这段死循环是一个阻塞的清理任务,首先进行清理,返回下次需要清理的间隔时间,当等待时间到了之后,再次进行清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Socket清理的Runnable,每当put操作时,就会被主动调用
//注意put操作是在网络线程
//而Socket清理是在`OkHttp ConnectionPool`线程池中调用
while (true) {
//执行清理并返回下场需要清理的时间
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
//在timeout内释放锁与时间片
ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
} catch (InterruptedException ignored) {
}
}
}
}

Connection的清除逻辑是什么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;

//遍历`Deque`中所有的`RealConnection`,标记泄漏的连接
synchronized (this) {
for (RealConnection connection : connections) {
// 查询此连接内部StreamAllocation的引用数量
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}

idleConnectionCount++;

//选择排序法,标记出空闲连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}

if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
//如果(`空闲socket连接超过5个`
//且`keepalive时间大于5分钟`)
//就将此泄漏连接从`Deque`中移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
//返回此连接即将到期的时间,供下次清理
//这里依据是在上文`connectionBecameIdle`中设定的计时
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//全部都是活跃的连接,5分钟后再次清理
return keepAliveDurationNs;
} else {
//没有任何连接,跳出循环
cleanupRunning = false;
return -1;
}
}

//关闭连接,返回`0`,也就是立刻再次清理
closeQuietly(longestIdleConnection.socket());
return 0;
}

清除逻辑:

  1. 遍历Deque中所有的RealConnection,标记泄漏的连接
  2. 如果被标记的连接满足(空闲socket连接超过5个&&keepalive时间大于5分钟),就将此连接从Deque中移除,并关闭连接,返回0,也就是将要执行wait(0),提醒立刻再次扫描
  3. 如果(目前还可以塞得下5个连接,但是有可能泄漏的连接(即空闲时间即将达到5分钟)),就返回此连接即将到期的剩余时间,供下次清理
  4. 如果(全部都是活跃的连接),就返回默认的keep-alive时间,也就是5分钟后再执行清理
  5. 如果(没有任何连接),就返回-1,跳出清理的死循环

如何标记并找到最不活跃的连接?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//类似于引用计数法,如果引用全部为空,返回立刻清理
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//虚引用列表
List<Reference<StreamAllocation>> references = connection.allocations;
//遍历弱引用列表
for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);
//如果正在被使用,跳过,接着循环
//是否置空是在上文`connectionBecameIdle`的`release`控制的
if (reference.get() != null) {
//非常明显的引用计数
i++;
continue;
}

//否则移除引用
references.remove(i);
connection.noNewStreams = true;

//如果所有分配的流均没了,标记为已经距离现在空闲了5分钟
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}

return references.size();
}
  1. 遍历Deque中所有的RealConnection,标记泄漏的连接
  2. 如果被标记的连接满足(空闲socket连接超过5个&&keepalive时间大于5分钟),就将此连接从Deque中移除,并关闭连接,返回0,也就是将要执行wait(0),提醒立刻再次扫描
  3. 如果(目前还可以塞得下5个连接,但是有可能泄漏的连接(即空闲时间即将达到5分钟)),就返回此连接即将到期的剩余时间,供下次清理
  4. 如果(全部都是活跃的连接),就返回默认的keep-alive时间,也就是5分钟后再执行清理
  5. 如果(没有任何连接),就返回-1,跳出清理的死循环