[关闭]
@natsumi 2017-10-08T13:58:48.000000Z 字数 24854 阅读 1561

OkHTTP 源码分析

Java Android


参考:http://www.jianshu.com/p/aad5aacd79bf
(原文作者:BlackSwift)

OkHttp是一个高效的Http客户端,有如下的特点:

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

主要对象

Connection: 对JDK中的物理socket进行了引用计数封装,用来控制socket连接
Stream: 维护HTTP的流,用来对Requset/Response进行IO操作
Call: HTTP请求任务封装
StreamAllocation: 用来控制Connections/Streams的资源分配与释放

工作流程的概述

当我们用OkHttpClient.newCall(request)进行execute/enenqueue时,实际是将请求Call放到了Dispatcher中,okhttp使用Dispatcher进行线程分发,它有两种方法,一个是普通的同步单线程;另一种是使用了队列进行并发任务的分发(Dispatch)与回调,我们下面主要分析第二种,也就是队列这种情况,这也是okhttp能够竞争过其它库的核心功能之一

任务队列

反向代理模型

在OkHttp中,使用了与Nginx类似的反向代理与分发技术,这是典型的单生产者多消费者问题。

我们知道在Nginx/SLB中,用户通过HTTP(Socket)访问前置的服务器,服务器会添加Header并自动转发请求给后端集群,接着返回数据结果给用户(比如简书上次挂了也显示了Nginx报错)。通过将工作分配给多个后台服务器并共享Redis的Session,可以提高服务的负载均衡能力,实现非阻塞、高可用、高并发连接,避免资源全部放到一台服务器而带来的负载,速度,在线率等影响。

而在OkHttp中,非常类似于上述场景,它使用Dispatcher作为任务的派发器,线程池对应多台后置服务器,用AsyncCall对应Socket请求,用Deque<readyAsyncCalls>对应Nginx的内部缓存

Dispatcher

Dispatcher维护了如下变量,用于控制并发的请求

maxRequests = 64: 最大并发请求数为64
maxRequestsPerHost = 5: 每个主机最大请求数为5
Dispatcher: 分发者,也就是生产者(默认在主线程)
AsyncCall: 队列中需要处理的Runnable(包装了异步回调接口)
ExecutorService:消费者池(也就是线程池)
Deque<readyAsyncCalls>:缓存等待区(用数组实现,可自动扩容,无大小限制)
Deque<runningAsyncCalls>:正在运行的任务,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存

当我们希望使用OkHttp的异步请求时,一般进行如下构造

  1. OkHttpClient client = new OkHttpClient.Builder().build();
  2. Request request = new Request.Builder()
  3. .url("http://qq.com").get().build();
  4. client.newCall(request).enqueue(new Callback() {
  5. @Override public void onFailure(Call call, IOException e) {
  6. }
  7. @Override public void onResponse(Call call, Response response) throws IOException {
  8. }
  9. });

当HttpClient的请求入队时,根据代码,我们可以发现实际上是Dispatcher进行了入队操作
根据生产者消费者模型的模型理论,当入队(enqueue)请求时,如果满足(runningRequests<64 && runningRequestsPerHost<5),那么就直接把AsyncCall直接加到runningCalls的队列中,并在线程池中执行(线程池会根据当前负载自动创建,销毁,缓存相应的线程)。反之,就放入readyAsyncCalls进行缓存等待。

  1. /**
  2. * 如果满足条件,那么就直接把AsyncCall直接加到runningCalls的队列中,并在线程池中执行
  3. * (线程池会根据当前负载自动创建,销毁,缓存相应的线程)。
  4. * 反之就放入readyAsyncCalls进行缓存等待。
  5. *
  6. * @param call
  7. */
  8. synchronized void enqueue(AsyncCall call) {
  9. if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
  10. runningAsyncCalls.add(call);
  11. executorService().execute(call);
  12. } else {
  13. readyAsyncCalls.add(call);
  14. }
  15. }

当任务执行完成后,无论是否有异常,finally代码段总会被执行,也就是会调用Dispatcher的finished函数

发现它将正在运行的任务Call从队列runningAsyncCalls中移除后,接着执行promoteCalls()函数

  1. /**
  2. * Used by {@code AsyncCall#run} to signal completion.
  3. * 从runningSyncCalls中移除call,并调用{@link #promoteCalls()},将开始一些正在等待的任务
  4. */
  5. void finished(AsyncCall call) {
  6. finished(runningAsyncCalls, call, true);
  7. }
  8. private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
  9. int runningCallsCount;
  10. Runnable idleCallback;
  11. synchronized (this) {
  12. if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
  13. if (promoteCalls) promoteCalls();
  14. runningCallsCount = runningCallsCount();
  15. idleCallback = this.idleCallback;
  16. }
  17. if (runningCallsCount == 0 && idleCallback != null) {
  18. idleCallback.run();
  19. }
  20. }

其中调用了promoteCalls()方法,手动移除运行区的一些call,再开始一些缓存等待区的call(可以看出这里是主动清理的,因此不会发生死锁)

  1. private void promoteCalls() {
  2. //如果目前是最大负荷运转,接着等
  3. if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
  4. //如果缓存等待区是空的,接着等
  5. if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
  6. for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
  7. AsyncCall call = i.next();
  8. // call的目标主机的正在执行的请求数 < maxRequestsPerHost
  9. if (runningCallsForHost(call) < maxRequestsPerHost) {
  10. // 将call移动到running的队列,并执行
  11. i.remove();
  12. runningAsyncCalls.add(call);
  13. executorService().execute(call);
  14. }
  15. if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
  16. }
  17. }

这样,就主动的把缓存队列向前走了一步,而没有使用互斥锁等复杂编码

我们再分析请求元素AsyncCall(它实现了抽象类 NamedRunnable,NamedRunnable实现了Runnable接口),它内部实现的execute(这个方法将在NamedRunnable的run中被调用)方法如下:

  1. @Override
  2. protected void execute() {
  3. boolean signalledCallback = false;
  4. try {
  5. // 执行耗时IO任务
  6. // 经过拦截器链的处理得到response
  7. Response response = getResponseWithInterceptorChain();
  8. if (retryAndFollowUpInterceptor.isCanceled()) {
  9. signalledCallback = true;
  10. //回调,注意这里回调是在线程池中,而不是想当然的主线程回调
  11. responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
  12. } else {
  13. signalledCallback = true;
  14. //回调,同上
  15. responseCallback.onResponse(RealCall.this, response);
  16. }
  17. } catch (IOException e) {
  18. if (signalledCallback) {
  19. // Do not signal the callback twice!
  20. Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
  21. } else {
  22. eventListener.callFailed(RealCall.this, e);
  23. responseCallback.onFailure(RealCall.this, e);
  24. }
  25. } finally {
  26. client.dispatcher().finished(this);
  27. }
  28. }

Summary

OkHttp采用Dispatcher技术,类似于Nginx,与线程池配合实现了高并发,低阻塞的运行。
OkHttp最出彩的地方就是在try/finally中调用了finished函数,可以主动控制等待队列的移动,而不是采用锁或者wait/notify,极大减少了编码复杂性。

Dispatcher持有一个消费者线程池和两个Deque。线程池消费AsycCall,Deque存储正在运行的任务和缓存等待的任务,按照入队的顺序先进先出。

复用连接池

作用

HTTP中的keepalive连接在网络性能优化中,对于延迟降低与速度提升的有非常重要的作用。

通常我们进行http连接时,首先进行tcp握手,然后传输数据,最后释放

这种方法的确简单,但是在复杂的网络内容中就不够用了,创建socket需要进行3次握手,而释放socket需要2次握手(或者是4次)。重复的连接与释放tcp连接就像每次仅仅挤1mm的牙膏就合上牙膏盖子接着再打开接着挤一样。而每次连接大概是TTL一次的时间(也就是ping一次),在TLS环境下消耗的时间就更多了。很明显,当访问复杂网络时,延时(而不是带宽)将成为非常重要的因素。

当然,上面的问题早已经解决了,在http中有一种叫做keepalive connections的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手

在现代浏览器中,一般同时开启6~8个keepalive connections的socket连接,并保持一定的链路生命,当不需要时再关闭;而在服务器中,一般是由软件根据负载情况(比如FD最大值、Socket内存、超时时间、栈内存、栈数量等)决定是否主动关闭。

Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)
当然keepalive也有缺点,在提高了单个客户端性能的同时,复用却阻碍了其他客户端的链路速度,具体来说如下

好了,以上科普完毕,本文主要是写客户端的,服务端不再介绍。下文假设服务器是经过专业的运维配置好的,它默认开启了keep-alive,并不主动关闭连接

涉及的概念和对象

主要有下面三个概念:

对应源码中关键的对象:

Instances of this class act on behalf of the call, using one or more streams over one or more connections.

RealConnection 源码分析

每个连接持有一个记录stream的list

  1. /**
  2. * Current streams carried by this connection.
  3. * 以下三个方法会修改这个list
  4. * {@link StreamAllocation#release()}
  5. * {@link StreamAllocation#acquire(RealConnection, boolean)}
  6. * {@link StreamAllocation#releaseAndAcquire(RealConnection)}
  7. */
  8. public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

还持有一个连接池

  1. private final ConnectionPool connectionPool;

连接池 ConnectionPool 源码分析

持有一个名为connections的Deque,用于保存所有连接。
相应的有get和put方法

  1. private final Deque<RealConnection> connections = new ArrayDeque<>();
  2. /**
  3. * Returns a recycled connection to {@code address}, or null if no such connection exists. The
  4. * route is null if the address has not yet been routed.
  5. */
  6. @Nullable
  7. RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
  8. assert (Thread.holdsLock(this));
  9. for (RealConnection connection : connections) {
  10. //如果这个connection可用
  11. if (connection.isEligible(address, route)) {
  12. // 在connection的allocations(引用记录列表List<Reference<StreamAllocation>>)
  13. // 中加入streamAllocation
  14. streamAllocation.acquire(connection, true);
  15. return connection;
  16. }
  17. }
  18. return null;
  19. }
  20. /**
  21. * 用户socket连接成功,向连接池中put新的socket
  22. * 会调用回收函数,线程池就会执行cleanupRunnable
  23. *
  24. * @param connection
  25. */
  26. void put(RealConnection connection) {
  27. assert (Thread.holdsLock(this));
  28. if (!cleanupRunning) {
  29. cleanupRunning = true;
  30. executor.execute(cleanupRunnable);
  31. }
  32. connections.add(connection);
  33. }

持有一个route数据库

  1. /**
  2. * 记录连接失败的Route的黑名单,当连接失败的时候就会把失败的线路加进去
  3. */
  4. final RouteDatabase routeDatabase = new RouteDatabase();

持有一个线程池,这个线程池是专门用来跑清理过期连接的线程的。

  1. /**
  2. * Background threads are used to cleanup expired connections. There will be at most a single
  3. * thread running per connection pool. The thread pool executor permits the pool itself to be
  4. * garbage collected.
  5. */
  6. private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
  7. Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
  8. new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

清理线程的工作如下

  1. /**
  2. * Socket清理的Runnable,每当put操作时,就会被主动调用
  3. * 注意put操作是在网络线程,而清理线程在线程池{@link ConnectionPool#executor}中调用
  4. */
  5. private final Runnable cleanupRunnable = new Runnable() {
  6. @Override
  7. public void run() {
  8. while (true) {
  9. // 执行清理并返回下次需要清理的间隔时间
  10. long waitNanos = cleanup(System.nanoTime());
  11. if (waitNanos == -1) return;//没有connection的情况,跳出循环
  12. if (waitNanos > 0) {
  13. long waitMillis = waitNanos / 1000000L;
  14. waitNanos -= (waitMillis * 1000000L);
  15. synchronized (ConnectionPool.this) {
  16. try {
  17. //在timeout内释放锁与时间片
  18. ConnectionPool.this.wait(waitMillis, (int) waitNanos);
  19. } catch (InterruptedException ignored) {
  20. }
  21. }
  22. }
  23. }
  24. }
  25. };

其中比较重要的是cleanup方法

  1. /**
  2. * Performs maintenance on this pool, evicting the connection that has been idle the longest if
  3. * either it has exceeded the keep alive limit or the idle connections limit.
  4. * <p>
  5. * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
  6. * -1 if no further cleanups are required.
  7. * <p>
  8. * 使用了类似于GC的标记-清除算法,也就是首先标记出最不活跃的连接(我们可以叫做泄漏连接,或者空闲连接),
  9. * 接着进行清除
  10. */
  11. long cleanup(long now) {
  12. int inUseConnectionCount = 0;
  13. int idleConnectionCount = 0;
  14. // 记录空闲时间最长的connection
  15. RealConnection longestIdleConnection = null;
  16. // 记录空闲时间最长的connection空闲的时间
  17. long longestIdleDurationNs = Long.MIN_VALUE;
  18. // Find either a connection to evict, or the time that the next eviction is due.
  19. synchronized (this) {
  20. // 遍历Deque中所有的RealConnection
  21. for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
  22. RealConnection connection = i.next();
  23. // If the connection is in use, keep searching.
  24. // 查询此连接内部StreamAllocation的引用数量,大于0就是in use,否则为空闲
  25. if (pruneAndGetAllocationCount(connection, now) > 0) {
  26. inUseConnectionCount++;
  27. continue;
  28. }
  29. idleConnectionCount++;
  30. // If the connection is ready to be evicted, we're done.
  31. // 找出空闲时间最长的connection
  32. long idleDurationNs = now - connection.idleAtNanos;
  33. if (idleDurationNs > longestIdleDurationNs) {
  34. longestIdleDurationNs = idleDurationNs;
  35. longestIdleConnection = connection;
  36. }
  37. }
  38. // 如果最长空闲时间大于keepAliveDurationNs(默认是5min)
  39. // 或者空闲connection数大于maxIdleConnections(默认为5)
  40. // 从connections中移除,并在下面关闭,返回0,即需要立即再次清理
  41. // 其他情况返回下次执行清理的时间,没有connection返回-1
  42. if (longestIdleDurationNs >= this.keepAliveDurationNs
  43. || idleConnectionCount > this.maxIdleConnections) {
  44. // We've found a connection to evict. Remove it from the list, then close it below (outside
  45. // of the synchronized block).
  46. connections.remove(longestIdleConnection);
  47. } else if (idleConnectionCount > 0) {
  48. // A connection will be ready to evict soon.
  49. return keepAliveDurationNs - longestIdleDurationNs;
  50. } else if (inUseConnectionCount > 0) {
  51. // All connections are in use. It'll be at least the keep alive duration 'til we run again.
  52. return keepAliveDurationNs;
  53. } else {
  54. // No connections, idle or in use.
  55. cleanupRunning = false;
  56. return -1;
  57. }
  58. }
  59. closeQuietly(longestIdleConnection.socket());
  60. // Cleanup again immediately.
  61. return 0;
  62. }

cleanup中调用的一个方法,查询此连接内部StreamAllocation的引用数量

  1. /**
  2. * Prunes any leaked allocations and then returns the number of remaining live allocations on
  3. * {@code connection}. Allocations are leaked if the connection is tracking them but the
  4. * application code has abandoned them. Leak detection is imprecise and relies on garbage
  5. * collection.
  6. * <p>
  7. * 清理connection中的allocation列表中已经为空(泄漏)的弱引用,返回非空的引用数
  8. */
  9. private int pruneAndGetAllocationCount(RealConnection connection, long now) {
  10. List<Reference<StreamAllocation>> references = connection.allocations;
  11. for (int i = 0; i < references.size(); ) {
  12. Reference<StreamAllocation> reference = references.get(i);
  13. //如果正在被使用,计数,跳过,接着循环
  14. if (reference.get() != null) {
  15. i++;
  16. continue;
  17. }
  18. // We've discovered a leaked allocation. This is an application bug.
  19. StreamAllocation.StreamAllocationReference streamAllocRef =
  20. (StreamAllocation.StreamAllocationReference) reference;
  21. String message = "A connection to " + connection.route().address().url()
  22. + " was leaked. Did you forget to close a response body?";
  23. Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
  24. //否则移除引用,且不再给这个connection分配新的Stream
  25. references.remove(i);
  26. connection.noNewStreams = true;
  27. // If this was the last allocation, the connection is eligible for immediate eviction.
  28. // 如果所有分配的流都泄漏然后被remove了
  29. // (这不是正常的空闲)标记已经空闲时间为keepAliveDurationNs(表示需要立即清理)
  30. if (references.isEmpty()) {
  31. connection.idleAtNanos = now - keepAliveDurationNs;
  32. return 0;
  33. }
  34. }
  35. // 返回清理后的引用数
  36. return references.size();
  37. }

总结

在okhttp中,在高层代码的调用中,使用了类似于引用计数的方式跟踪Socket流的调用,这里的计数对象是StreamAllocation,它被反复执行aquire与release操作,这两个函数其实是在改变Connection中的List>大小。List中Allocation的数量也就是物理socket被引用的计数(Refference Count),如果计数为0的话,说明此连接没有被使用,是空闲的,从而被线程池监测到并回收;如果上层代码仍然引用,就不需要关闭连接。这样就可以保持多个健康的keep-alive连接。

DiskLruCache

缓存,顾名思义,也就是方便用户快速的获取值的一种储存方式。小到与CPU同频的昂贵的缓存颗粒,内存,硬盘,网络,CDN反代缓存,DNS递归查询,OS页面置换,Redis数据库,都可以看作缓存。它有如下的特点:

在OkHttp中,使用FileSystem作为缓存载体(磁盘相对于网络的缓存),使用LRU作为页面置换算法(封装了LinkedHashMap)。

DiskLruCache维护着文件的创建,清理,读取。内部有清理线程池,LinkedHashMap(也就是LruCache)

LinkedHashMap

LinkedHashMap有一个accessOrder属性,若为true,则迭代顺序会按访问顺序调整。可以用作缓存。在get元素时,如果设置accessOrder为true时,通过调用afterNodeAccess移动元素到链尾。afterNodeInsertion在put和putAll后被调用,根据evict和removeEldestEntry的返回值,可能会删除最最靠近head的元素。

  1. // move node to last
  2. void afterNodeAccess(Node<K,V> e)
  3. // possibly remove eldest
  4. void afterNodeInsertion(boolean evict)

more
https://github.com/xietiantian/JCFInternals/blob/master/markdown/7-LinkedHashSet%20and%20LinkedHashMap.md

OkHttp的文件系统

OkHttp中的关键对象如下:

FileSystem

使用Okio对File的封装,简化了IO操作。
众所周之,文件读写是流操作,是一大堆的令人头痛的try/cache操作,在OkHttp中设计了FileSystem.SYSTEM作为文件层的管理。通过用Okio库中的Source/Sink对File进行包装,而不用更为头痛的InputStream这类东西,使上层调用与管道操作一样简单。

File(低级操作,步骤繁琐) -> Okio(封装) -> FileSystem(友好工具类)

代码实现上,FileSystem是一个接口,FileSystem.SYSTEM通过静态内部类的形式实现了FileSystem接口

  1. public interface FileSystem {
  2. /**
  3. * The host machine's local file system.
  4. */
  5. FileSystem SYSTEM = new FileSystem() {
  6. @Override
  7. public Source source(File file) throws FileNotFoundException {
  8. return Okio.source(file);
  9. }
  10. // ...
  11. }
  12. // ...
  13. }

文件高级封装(DiskLruCache.Entry/Editor/Snapshot)

DiskLruCache.Editor: 添加了同步锁,并对FileSystem进行高度封装

DiskLruCache.Entry: 维护着key对应的多个文件
本部分进行了如下的转换,进行了实际的put/get操作

FileSystem <-- DiskLruCache.Entry/Editor --> source/sink(更少参数)

DiskLruCache.Entry针对每个请求的url对应的文件进行引用维护(而没有进行创建/读取等操作),它内部维护了2个File数组(一个cleanFiles,一个dirtyFiles),一般来说每个url对应2~4个文件。 文件名命名规则是{md5(url)+ {0,1}},后面的0或1,分别表示ENTRY_METADATA与ENTRY_BODY。

通过这里我们也明白,后端可以通过 common.js?v=a4sdjy3进行配置queryString。因为md5会计算全部url,版本更新后,md5也会变化,这样就避免了服务器新文件上线而缓存还是旧的问题

DiskLruCache.Editor对工具类FileSystem进行进一步的封装,它以DiskLruCache.Entry作为构造参数,通过操控Entry中维护的数组,对外暴露newSource/newSink,为上层的java对象与文件的转换提供基于okio的流操作。

序列化与反序列化(Cache.Entry)

文件存储本质上也是Response对象与Okio流序列化与反序列化的过程。本部分提供了下图的转变

Resonse(java对象) <--- Cache.Entry ---> source/sink(文件io)

如果信息本身就是二进制,就直接写到文件中;如果是文本信息,按照预设的格式写入即可。

缓存的自动清理

在DiskLruCache初始化时,将建立线程池。

  1. /**
  2. * Create a cache which will reside in {@code directory}. This cache is lazily initialized on
  3. * first access and will be created if it does not exist.
  4. *
  5. * @param directory a writable directory
  6. * @param valueCount the number of values per cache entry. Must be positive.
  7. * @param maxSize the maximum number of bytes this cache should use to store
  8. */
  9. public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
  10. int valueCount, long maxSize) {
  11. if (maxSize <= 0) {
  12. throw new IllegalArgumentException("maxSize <= 0");
  13. }
  14. if (valueCount <= 0) {
  15. throw new IllegalArgumentException("valueCount <= 0");
  16. }
  17. // Use a single background thread to evict entries.
  18. // 在DiskLruCache初始化时,将建立线程池,最少零个线程,最大一个线程,
  19. // 线程空闲可以活60s,线程名叫做"OkHttp DiskLruCache",当JVM退出时,线程自动结束。
  20. Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
  21. new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
  22. return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
  23. }

当需要清理时,执行清理任务,它将在每次get/set后即其他需要清理的时刻调用

  1. private final Runnable cleanupRunnable = new Runnable() {
  2. public void run() {
  3. synchronized (DiskLruCache.this) {
  4. if (!initialized | closed) {
  5. return; // Nothing to do
  6. }
  7. try {
  8. // 遍历LRU缓存(从旧到新进行遍历map),并删除文件
  9. // 直到小于MaxSize为止
  10. trimToSize();
  11. } catch (IOException ignored) {
  12. mostRecentTrimFailed = true;
  13. }
  14. try {
  15. if (journalRebuildRequired()) {
  16. rebuildJournal();
  17. redundantOpCount = 0;
  18. }
  19. } catch (IOException e) {
  20. mostRecentRebuildFailed = true;
  21. journalWriter = Okio.buffer(Okio.blackhole());
  22. }
  23. }
  24. }
  25. };

总结

缓存策略

http的缓存Header

在分析源码之前,我们先回顾一下http的缓存Header的含义

表示到期时间,一般用在response报文中,当超过此事件后响应将被认为是无效的而需要网络连接,反之而是直接使用缓存

Expires: Thu, 12 Jan 2017 11:01:33 GMT

相对值,单位是秒,指定某个文件被续多少秒的时间,从而避免额外的网络请求。比expired更好的选择,它不用要求服务器与客户端的时间同步,也不用服务器时刻同步修改配置Expired中的绝对时间,而且它的优先级比Expires更高。比如简书静态资源有如下的header,表示可以续31536000秒,也就是一年。

Cache-Control: max-age=31536000, public

如果我们通过设置header保证了客户端可以缓存的,而此时远程服务器更新了文件如何解决呢?我们这时可以通过修改url中的文件名版本后缀进行缓存,这个方法是最简单的,实践性非常高。

比如下文是又拍云的公共CDN就提供了多个版本的JQuery
upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.3.min.js

如缓存过期或者强制放弃缓存,在此情况下,缓存策略全部交给服务器判断,客户端只用发送条件get请求即可,如果缓存是有效的,则返回304 Not Modifiled,否则直接返回body。

请求的方式有两种:

Last-Modified-Date

客户端第一次网络请求时,服务器返回了
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
客户端再次请求时,通过发送
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
交给服务器进行判断,如果仍然可以缓存使用,服务器就返回304

ETag

ETag是对资源文件的一种摘要,客户端并不需要了解实现细节。

当客户端第一请求时,服务器返回了
ETag: "5694c7ef-24dc"
客户端再次请求时,通过发送
If-None-Match:"5694c7ef-24dc"
交给服务器进行判断,如果仍然可以缓存使用,服务器就返回304

如果 ETag 和 Last-Modified 都有,则必须一次性都发给服务器,它们没有优先级之分,反正这里客户端没有任何判断的逻辑。

no-cache/no-store: 不使用缓存,no-cache指令的目的是防止从缓存中返回过期的资源。客户端发送的请求中如果包含no-cache指令的话,表示客户端将不会接受缓存过的相应,于是缓存服务器必须把客户端请求转发给源服务器。服务器端返回的相应中包含no-cache指令的话那么缓存服务器不能对资源进行缓存。

only-if-cached: 只使用缓存,这个标签只在请求中使用,表示无论是否有网完全只使用缓存(如果命中还好说,否则返回503错误/网络错误),这个标签比较危险。

Date: The date and time that the message was sent

Age: The Age response-header field conveys the sender's estimate of the amount of time since the response (or its revalidation) was generated at the origin server. 说人话就是CDN反代服务器到原始服务器获取数据延时的缓存时间

全部标签看这里
https://en.wikipedia.org/wiki/List_of_HTTP_header_fields

源码分析

OkHttp中使用了CacheStrategy实现了HTTP的缓存策略,它根据之前的缓存结果与当前将要发送Request的header进行策略分析,并得出是否进行请求的结论。

主要涉及两个类,CacheInterceptor和CacheStrategy。

Interceptor & Chain

Interceptor是一个接口,除了内部包含了一个Chain接口(RealInterceptorChain是OKHTTP提供的唯一实现类)外只有一个intercept方法。综合代码,每个Interceptor的实现类相当于HTTP请求、响应过程中的一个处理模块,由RealInterceptorChain把他们串起来。

  1. public interface Interceptor {
  2. Response intercept(Chain chain) throws IOException;
  3. interface Chain{...}
  4. }

回顾一下请求执行的源码,Dispatcher的消费者线程池执行的是AsyncCall,线程的run方法中会执行AsyncCall的excute方法,在excute方法中是通过getResponseWithInterceptorChain()方法执行耗时操作的。展开这个方法,这大概是个责任链模式?

  1. /**
  2. * 拦截器是okhttp中强大的流程装置,它可以用来监控log,修改/消费请求,修改结果,甚至是对用户透明的GZIP压缩。
  3. * 这个方法中递归调用拦截器,然后得到response
  4. *
  5. * @return
  6. * @throws IOException
  7. */
  8. Response getResponseWithInterceptorChain() throws IOException {
  9. // Build a full stack of interceptors.
  10. List<Interceptor> interceptors = new ArrayList<>();
  11. interceptors.addAll(client.interceptors());
  12. interceptors.add(retryAndFollowUpInterceptor);
  13. interceptors.add(new BridgeInterceptor(client.cookieJar()));
  14. interceptors.add(new CacheInterceptor(client.internalCache()));
  15. // 其拦截方法中会调用streamAllocation.newStream的创建httpCodec
  16. interceptors.add(new ConnectInterceptor(client));
  17. if (!forWebSocket) {
  18. interceptors.addAll(client.networkInterceptors());
  19. }
  20. // CallServerInterceptor是最后一个拦截器,其intercept方法中会调用
  21. // httpCodec.writeRequestHeaders 和
  22. // httpCodec.readResponseHeaders
  23. interceptors.add(new CallServerInterceptor(forWebSocket));
  24. // index为0的RealInterceptorChain中,httpCodec为空
  25. // ConnectInterceptor的intercept中调用RealInterceptorChain的proceed方法时
  26. // 传入了httpCodec,即再为下一个拦截器创建链的时候httpCodec不为null
  27. Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
  28. originalRequest, this, eventListener, client.connectTimeoutMillis(),
  29. client.readTimeoutMillis(), client.writeTimeoutMillis());
  30. return chain.proceed(originalRequest);
  31. }

一个名为interceptors的list存储了链上所有的拦截器,然后创建了一个链对象,并调用了RealInterceptorChain的proceed方法。去掉了一些异常处理的代码后,处理流程很清晰

  1. /**
  2. * 其实是个递归方法
  3. *
  4. * @param request
  5. * @param streamAllocation
  6. * @param httpCodec
  7. * @param connection
  8. * @return
  9. * @throws IOException
  10. */
  11. public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
  12. RealConnection connection) throws IOException {
  13. //一些异常情况
  14. //...
  15. // Call the next interceptor in the chain.
  16. // 创建下一个(index+1)拦截器的RealInterceptorChain
  17. RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
  18. connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
  19. writeTimeout);
  20. // 获取当前(index)的拦截器
  21. Interceptor interceptor = interceptors.get(index);
  22. // 执行当前拦截器的intercept方法,传入下一个(index+1)拦截器的RealInterceptorChain
  23. // 除了CallServerInterceptor(链上的最后一个拦截器)
  24. // 其他拦截器的intercept方法都会调用next的proceed方法
  25. Response response = interceptor.intercept(next);
  26. //一些异常情况
  27. //...
  28. return response;
  29. }

这里面调用了当前(index)拦截器的intercept方法,可以去翻一下getResponseWithInterceptorChain()方法中list里的几个拦截器的源码,可以发现多数拦截器的intercept方法中都会调用传入chain的proceed方法,形成了一种类似递归的调用。

如果这是链上最后的拦截器(比如CallServerInterceptor,或者缓存中可以得到response,CacheInterceptor也可能是最后一个拦截器),intercept会直接返回response,而不调用chain的proceed方法。

CacheInterceptor

缓存也是链上的一个拦截器,其intercept方法如下

  1. @Override
  2. public Response intercept(Chain chain) throws IOException {
  3. // 从cache中获取cacheCandidate(将作为CacheStrategy.Factory的cacheResponse)
  4. Response cacheCandidate = cache != null
  5. ? cache.get(chain.request())
  6. : null;
  7. long now = System.currentTimeMillis();
  8. // CacheStrategy.Factory构造方法中解析cacheCandidate(cacheResponse)的header然后把解析到的
  9. // value保存到Factory的相应属性里。主要是缓存相关的字段。
  10. CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate)
  11. .get();//根据request(部分情况会给request添加缓存的header字段)和response返回strategy
  12. Request networkRequest = strategy.networkRequest;
  13. Response cacheResponse = strategy.cacheResponse;
  14. if (cache != null) {
  15. cache.trackResponse(strategy);
  16. }
  17. if (cacheCandidate != null && cacheResponse == null) {
  18. closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
  19. }
  20. // If we're forbidden from using the network and the cache is insufficient, fail.
  21. // only-if-cached(表明不进行网络请求,且缓存不存在或者过期,返回504错误)
  22. if (networkRequest == null && cacheResponse == null) {
  23. return new Response.Builder()
  24. .request(chain.request())
  25. .protocol(Protocol.HTTP_1_1)
  26. .code(504)
  27. .message("Unsatisfiable Request (only-if-cached)")
  28. .body(Util.EMPTY_RESPONSE)
  29. .sentRequestAtMillis(-1L)
  30. .receivedResponseAtMillis(System.currentTimeMillis())
  31. .build();
  32. }
  33. // If we don't need the network, we're done.
  34. // 不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络
  35. // 不再创建下一个拦截器的链,递归不再深入,开始一路返回
  36. if (networkRequest == null) {
  37. return cacheResponse.newBuilder()
  38. .cacheResponse(stripBody(cacheResponse))
  39. .build();
  40. }
  41. // 下面是需要网络访问的情况
  42. Response networkResponse = null;
  43. try {
  44. networkResponse = chain.proceed(networkRequest);
  45. } finally {
  46. // If we're crashing on I/O or otherwise, don't leak the cache body.
  47. if (networkResponse == null && cacheCandidate != null) {
  48. closeQuietly(cacheCandidate.body());
  49. }
  50. }
  51. // If we have a cache response too, then we're doing a conditional get.
  52. if (cacheResponse != null) {
  53. if (networkResponse.code() == HTTP_NOT_MODIFIED) {
  54. Response response = cacheResponse.newBuilder()
  55. .headers(combine(cacheResponse.headers(), networkResponse.headers()))
  56. .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
  57. .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
  58. .cacheResponse(stripBody(cacheResponse))
  59. .networkResponse(stripBody(networkResponse))
  60. .build();
  61. networkResponse.body().close();
  62. // Update the cache after combining headers but before stripping the
  63. // Content-Encoding header (as performed by initContentStream()).
  64. cache.trackConditionalCacheHit();
  65. cache.update(cacheResponse, response);
  66. return response;
  67. } else {
  68. closeQuietly(cacheResponse.body());
  69. }
  70. }
  71. Response response = networkResponse.newBuilder()
  72. .cacheResponse(stripBody(cacheResponse))
  73. .networkResponse(stripBody(networkResponse))
  74. .build();
  75. if (cache != null) {
  76. if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
  77. // Offer this request to the cache.
  78. CacheRequest cacheRequest = cache.put(response);
  79. return cacheWritingResponse(cacheRequest, response);
  80. }
  81. if (HttpMethod.invalidatesCache(networkRequest.method())) {
  82. try {
  83. cache.remove(networkRequest);
  84. } catch (IOException ignored) {
  85. // The cache cannot be written.
  86. }
  87. }
  88. }
  89. return response;
  90. }

没有删节比较长,主要看注释吧~
先从cache中搞一个cacheResponse出来,然后通过CacheStrategy得到这俩,networkRequest与cacheResponse,根据是否为空执行不同的请求判断后续处理方法,可能直接返回cacheResponse、还是网络请求、或者。。。

  1. Request networkRequest = strategy.networkRequest;
  2. Response cacheResponse = strategy.cacheResponse;

看2.1节的第二个表格
http://www.jianshu.com/p/9cebbbd0eeab

CacheStrategy

CacheStrategy的工作主要是这一句

  1. CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate)
  2. .get();

Factory的构造方法解析cacheCandidate(cacheResponse)的header然后把解析到的value保存到Factory的相应属性里。主要是缓存相关的字段。

  1. public Factory(long nowMillis, Request request, Response cacheResponse) {
  2. this.nowMillis = nowMillis;
  3. this.request = request;
  4. this.cacheResponse = cacheResponse;
  5. if (cacheResponse != null) {
  6. this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
  7. this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
  8. Headers headers = cacheResponse.headers();
  9. for (int i = 0, size = headers.size(); i < size; i++) {
  10. String fieldName = headers.name(i);
  11. String value = headers.value(i);
  12. if ("Date".equalsIgnoreCase(fieldName)) {
  13. servedDate = HttpDate.parse(value);
  14. servedDateString = value;
  15. } else if ("Expires".equalsIgnoreCase(fieldName)) {
  16. expires = HttpDate.parse(value);
  17. } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
  18. lastModified = HttpDate.parse(value);
  19. lastModifiedString = value;
  20. } else if ("ETag".equalsIgnoreCase(fieldName)) {
  21. etag = value;
  22. } else if ("Age".equalsIgnoreCase(fieldName)) {
  23. ageSeconds = HttpHeaders.parseSeconds(value, -1);
  24. }
  25. }
  26. }
  27. }

然后get方法根据request(部分情况会给request添加缓存的header字段)和response返回strategy,其实缓存策略就是http缓存header在端上表现的逻辑。具体这里就不展开了。

详细的看这个2.2节
http://www.jianshu.com/p/9cebbbd0eeab

总结

okhttp实现的缓存策略实质上就是大量的if判断集合,这些是根据RFC标准文档写死的,并没有相当难的技巧。

Okhttp的缓存是自动完成的,完全由服务器Header决定的,自己没有必要进行控制。

网上热传的文章在Interceptor中手工添加缓存代码控制,它固然有用,但是属于Hack式的利用,违反了RFC文档标准,不建议使用,OkHttp的官方缓存控制在注释中。如果读者的需求是对象持久化,建议用文件储存或者数据库即可(比如realm)。

服务器的配置非常重要,如果你需要减小请求次数,建议直接找对接人员对max-age等头文件进行优化;服务器的时钟需要严格NTP同步

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注