OkHttp3源码解析(二)——拦截器链和缓存策略

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 从源码分析OkHttp3的原理

OKHttp3源码解析系列

上一篇文章中我们介绍了OkHttp3的同步和异步请求流程,我们分析到不论是同步还是异步请求,都是通过RealCall内部的getResponseWithInterceptorChain方法来执行具体的网络连接的,下面我们来分析看看OkHttp3具体是怎么进行网络请求的。

OkHttp3的拦截器链

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    //这里参数0,表示的是interceptors列表中的索引。
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
}

getResponseWithInterceptorChain方法中添加了一系列的拦截器Interceptor,包括用户的拦截器等。然后调用了RealInterceptorChain的proceed方法。每个拦截器都有特定的作用,通过责任链模式,每个拦截器完成自己的任务后,不断调用下个拦截器,最后完成网络请求。

//RealInterceptorChain.class
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    ...

    // Call the next interceptor in the chain.
    //这里index+1,所以就会调用interceptors中的下一个拦截器
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    ...

    return response;
}

这里可以看到在RealInterceptorChain的proceed方法中,会逐个调用interceptors中的拦截器,调用顺序就是前面添加的顺序。所以RealInterceptorChain可以看作是整个拦截器链的控制器,通过它可以调用链上的下一个拦截器,而每一个拦截器就相当于链上的一环。

(1)用户拦截器:通过Builder的addInterceptor方法添加的拦截器。

(2)RetryAndFollowUpInterceptor:负责失败自动重连和必要的重定向。

(3)BridgeInterceptor:负责将用户的Request转换成一个实际的网络请求Request,再调用下一个拦截器获取Response,然后将Response转换成用户的Response。

(4)CacheInterceptor:负责控制缓存,缓存的逻辑就在这里面。

(5)ConnectInterceptor:负责进行连接主机,在这里会完成socket连接,并将连接返回。

(6)CallServerInterceptor:和服务器通信,完成Http请求。

所以我们可以总结出网络请求的调用流程:

(1)RealInterceptorChain拦截器链的proceed方法-->用户拦截器-->RetryAndFollowUpInterceptor-->BridgeInterceptor-->CacheInterceptor-->ConnectInterceptor-->CallServerInterceptor

(2)当然在CacheInterceptor中,如果发现当前的请求有缓存的话就会直接返回Response了,而不会走后面的调用链。


OkHttp3的缓存策略

上面提到拦截器中有个缓存拦截器CacheInterceptor,它里面主要包含的是缓存的逻辑。下面我们来看看这个缓存策略是怎么样的。

首先我们看CacheInterceptor的构造函数

public final class CacheInterceptor implements Interceptor {
  final InternalCache cache;

  public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
  }
}

从文章开头中我们在RealCall内部添加CacheInterceptor拦截器的方式可知,CacheInterceptor中的InternalCache来自OkHttpClient中

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
    ...
    final @Nullable Cache cache;
    final @Nullable InternalCache internalCache;

    OkHttpClient(Builder builder) {
        //我们在RealCall中初始化CacheInterceptor时调用的就是这个方法
        InternalCache internalCache() {
            return cache != null ? cache.internalCache : internalCache;
        }
    }
    
    public static final class Builder {
    
        @Nullable Cache cache;
        @Nullable InternalCache internalCache;
        
        /** Sets the response cache to be used to read and write cached responses. */
        //这里需要注意的是这个方法并不是public的
        void setInternalCache(@Nullable InternalCache internalCache) {
          this.internalCache = internalCache;
          this.cache = null;
        }

        /** Sets the response cache to be used to read and write cached responses. */
        public Builder cache(@Nullable Cache cache) {
          this.cache = cache;
          this.internalCache = null;
          return this;
        }
    }
}

从上面的OkHttpClient的源码中我们可以得出以下结论:

(1)OkHttpClient中有2个跟缓存有关的变量,一个是Cache,一个是internalCache。其中我们可以通过Builder来设置Cache,但是不能设置internalCache。

(2)从上面可以看出,默认Cache和internalCache都是null,也就是OkHttpClient没有默认的缓存实现。

(3)缓存拦截器CacheInterceptor中的internalCache来自OkHttpClient的Cache,因为OkHttpClient中的internalCache一直是null,我们没法从外界设置,所以如果我们没有为OkHttpClient设置Cache,那么缓存拦截器中的internalCache就也为null了,也就没法提供缓存功能。

从上面的源码中我们还发现,internalCache虽然不能从外界设置,但是它却是cache的一个内部变量。下面我们来具体看看缓存Cache的实现。

缓存Cache

public final class Cache implements Closeable, Flushable {
    ...
    //internalCache的方法内部调用的都是Cache的方法
    final InternalCache internalCache = new InternalCache() {
        @Override 
        public Response get(Request request) throws IOException {
          return Cache.this.get(request);
        }
        
        @Override 
        public CacheRequest put(Response response) throws IOException {
          return Cache.this.put(response);
        }
        
        @Override 
        public void remove(Request request) throws IOException {
          Cache.this.remove(request);
        }
        
        @Override 
        public void update(Response cached, Response network) {
          Cache.this.update(cached, network);
        }
        
        @Override 
        public void trackConditionalCacheHit() {
          Cache.this.trackConditionalCacheHit();
        }
        
        @Override 
        public void trackResponse(CacheStrategy cacheStrategy) {
          Cache.this.trackResponse(cacheStrategy);
        }
    };
    //缓存是用DiskLruCache实现的
    final DiskLruCache cache;
    
    ...
    
    public Cache(File directory, long maxSize) {
        this(directory, maxSize, FileSystem.SYSTEM);
    }
    
    Cache(File directory, long maxSize, FileSystem fileSystem) {
        this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
    }
    //缓存为key-value形式,其中key为请求的URL的md5值
    public static String key(HttpUrl url) {
        return ByteString.encodeUtf8(url.toString()).md5().hex();
    }

    @Nullable 
    Response get(Request request) {
        String key = key(request.url());
        DiskLruCache.Snapshot snapshot;
        Entry entry;
        try {
          snapshot = cache.get(key);
          if (snapshot == null) {
            return null;
          }
        } catch (IOException e) {
          // Give up because the cache cannot be read.
          return null;
        }
    
        ...
    
        return response;
    }
  
    ...
}

这一看我们就明白了,internalCache的确是Cache的一个内部变量,我们设置了Cache也就有了internalCache。而实际上,internalCache是一个接口类型的变量,它的一系列get、put方法,都是调用的Cache的方法,这也是外观模式的一种典型应用。当然这里internalCache从名字也可以看出是给内部其他对象调用的,所以internalCache和Cache的职责很明确,Cache供外部设置,而internalCache供内部调用。

同时我们看到Cache内部是通过DiskLruCache来实现缓存的,缓存的key就是request的URL的md5值,缓存的值就是Response。我们设置自己的缓存时,可以通过Cache的构造函数传入我们想要存放缓存的文件路径,以及缓存文件大小即可。比如:

OkHttpClient.Builder builder = new OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(20, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .cache(new Cache(context.getExternalCacheDir(), 10*1024*1024));

下面我们回到缓存拦截器CacheInterceptor中,看看具体的缓存逻辑。主要的逻辑都在intercept方法中。

CacheInterceptor的缓存逻辑

@Override 
public Response intercept(Chain chain) throws IOException {
    //如果我们没有设置缓存或是当前request没有缓存,那么cacheCandidate就为null了
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //如果我们没有设置缓存,或是当前request没有缓存,那么cacheCandidate就为null
    //获取具体的缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //后面会根据networkRequest和cacheResponse是否为空来做相应的操作
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    
    ...
}

可以看到intercept方法中会调用缓存策略工厂的get方法获取缓存策略CacheStrategy。我们进入CacheStrategy看看

public final class CacheStrategy {
    /** The request to send on the network, or null if this call doesn't use the network. */
    public final @Nullable Request networkRequest;
    
    /** The cached response to return or validate; or null if this call doesn't use a cache. */
    public final @Nullable Response cacheResponse;
    
    CacheStrategy(Request networkRequest, Response cacheResponse) {
        this.networkRequest = networkRequest;
        this.cacheResponse = cacheResponse;
    }
    ...
    public static class Factory {
        final long nowMillis;
        final Request request;
        final Response cacheResponse;
        
        ...
        public Factory(long nowMillis, Request request, Response cacheResponse) {
        
            this.nowMillis = nowMillis;
            this.request = request;
            this.cacheResponse = cacheResponse;
            ...
        }
        
        public CacheStrategy get() {
            CacheStrategy candidate = getCandidate();
    
            if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
                // We're forbidden from using the network and the cache is insufficient.
                return new CacheStrategy(null, null);
            }
    
            return candidate;
        }
        
        //根据不同的情况返回CacheStrategy
        private CacheStrategy getCandidate() {
            // No cached response.
            //如果缓存中的Response为null
            if (cacheResponse == null) {
                return new CacheStrategy(request, null);
            }
        
            // Drop the cached response if it's missing a required handshake.
            //如果缓存中的response缺少必要的握手信息
            if (request.isHttps() && cacheResponse.handshake() == null) {
                return new CacheStrategy(request, null);
            }
              
            //根据request和response是否能被缓存来生成CacheStrategy
            if (!isCacheable(cacheResponse, request)) {
                return new CacheStrategy(request, null);
            }
              
            CacheControl requestCaching = request.cacheControl();
            //如果Request中的noCache标志位为true或是request的请求头中包含"If-Modified-Since"或是"If-None-Match"标志位
            if (requestCaching.noCache() || hasConditions(request)) {
                return new CacheStrategy(request, null);
            }
            
            //如果缓存的response中的immutable标志位为true,则不请求网络
            CacheControl responseCaching = cacheResponse.cacheControl();
            if (responseCaching.immutable()) {
                return new CacheStrategy(null, cacheResponse);
            }
            ...
        }
    }
}

可以看到在CacheStrategy的内部工厂类Factory中有一个getCandidate方法,会根据具体的情况生成CacheStrategy类返回,是个典型的简单工厂模式。生成的CacheStrategy中有2个变量,networkRequest和cacheResponse,如果networkRequest为null,则表示不进行网络请求;而如果cacheResponse为null,则表示没有有效的缓存。

当我们没有设置缓存Cache时,显然cacheResponse始终都会为null。下面我们继续看intercept中的方法。

//如果networkRequest和cacheResponse都为null,则表示不请求网络而缓存又为null,那就返回504,请求失败
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(Util.EMPTY_RESPONSE)
      .sentRequestAtMillis(-1L)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
}

//如果不请求网络,但是存在缓存,那就直接返回缓存,不用请求网络
if (networkRequest == null) {
    return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

//否则的话就请求网络,就会调用下一个拦截器链,将请求转发到下一个拦截器
Response networkResponse = null;
try {
    networkResponse = chain.proceed(networkRequest);
} finally {
    // If we're crashing on I/O or otherwise, don't leak the cache body.
    if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
    }
}

根据上述源码我们可以总结出缓存拦截器中的缓存策略:

(1)首先会尝试从缓存Cache中获取当前请求request的缓存response,并根据传入的请求request和获取的缓存response通过缓存策略对象CacheStrategy的工厂类的get方法生成缓存策略类CacheStrategy。

(2)CacheStrategy的工厂类的get方法里面会根据一些规则生成CacheStrategy,这里面的规则实际上控制着缓存拦截器CacheInterceptor的处理逻辑。而这些规则都是根据请求的Request和缓存的Response的header头部信息来生成的,比如是否有noCache标志位,是否是immutable不可变的,以及缓存是否过期等。

(3)CacheInterceptor会根据CacheStrategy中的networkRequest和cacheResponse是否为空,来判断是请求网络还是直接使用缓存。

总结

(1)拦截器链和缓存策略都是OkHttp3的亮点所在,此外还有复用连接池等

(2)拦截器链通过责任链的模式,将网络请求过程中的职责功能都分割开,分别用不同的拦截器来完成失败重连、缓存处理、网络连接等问题。而且用户还可以添加自定义的拦截器,非常灵活,满足面向对象的开闭原则。

(3)缓存策略指的是对于请求的响应的缓存。OkHttp中有专门类Cache来实现缓存,Cache中采用了DiskLruCache,以Request的URL的md5为key,相应Response为value。此外Cache中还通过外观模式对外提供了InternalCache接口变量,用于调用Cache中的方法,也满足面向对象的接口隔离原则和依赖倒置原则等。

(4)缓存拦截器中会根据生成的缓存策略类CacheStrategy的2个变量networkRequest和cacheResponse来决定是连接网络还是直接使用缓存。如果2个变量都为空,则直接返回504,请求失败;如果缓存不为空,则直接返回缓存;如果networkRequest不为空,就通过调用RealInterceptorChain的proceed方法将请求继续转发到下一个拦截器。



不错过每一点精彩,欢迎扫码关注

AntDream

目录
相关文章
|
7天前
|
存储 缓存 监控
利用 Redis 缓存特性避免缓存穿透的策略与方法
【10月更文挑战第23天】通过以上对利用 Redis 缓存特性避免缓存穿透的详细阐述,我们对这一策略有了更深入的理解。在实际应用中,我们需要根据具体情况灵活运用这些方法,并结合其他技术手段,共同保障系统的稳定和高效运行。同时,要不断关注 Redis 缓存特性的发展和变化,及时调整策略,以应对不断出现的新挑战。
33 10
|
3天前
|
Web App开发 缓存 UED
如何设置浏览器的缓存策略?
【10月更文挑战第23天】通过合理地设置浏览器的缓存策略,可以在提高网页性能、减少网络流量的同时,确保用户能够获取到最新的内容,从而提升用户体验和网站的性能优化效果。
25 4
|
4天前
|
存储 消息中间件 缓存
缓存策略
【10月更文挑战第25天】在实际应用中,还需要不断地监控和调整缓存策略,以适应系统的变化和发展。
|
4天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
21 3
|
7天前
|
缓存 监控 NoSQL
Redis 缓存穿透及其应对策略
【10月更文挑战第23天】通过以上对 Redis 缓存穿透的详细阐述,我们对这一问题有了更深入的理解。在实际应用中,我们需要根据具体情况综合运用多种方法来解决缓存穿透问题,以保障系统的稳定运行和高效性能。同时,要不断关注技术的发展和变化,及时调整策略,以应对不断出现的新挑战。
26 4
|
11天前
|
存储 缓存 NoSQL
保持HTTP会话状态:缓存策略与实践
保持HTTP会话状态:缓存策略与实践
|
21天前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
48 5
|
23天前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
|
30天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
30天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)

推荐镜像

更多