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

简介: 从源码分析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

目录
相关文章
|
14天前
|
XML Java Android开发
Android实现自定义进度条(源码+解析)
Android实现自定义进度条(源码+解析)
47 1
|
25天前
|
存储 缓存 算法
缓存淘汰策略
缓存淘汰策略
30 0
|
26天前
|
人工智能 搜索推荐 安全
打造精准营销!营销电子邮件以客户为中心策略解析!
营销电子邮件是数字营销的核心,用于建立客户关系、推广产品和服务,提高品牌忠诚度和转化率。它们在客户旅程中扮演关键接触点角色,如欢迎邮件、购物车提醒和个性化推荐。电子邮件营销能提升品牌知名度,细分营销可带来760%的收入增长。然而,大量邮件可能导致邮件过载,缺乏个性化可能引起收件人反感,甚至网络安全问题。收件人和IT团队可通过过滤、优化设置、启用2FA等措施改善体验。营销团队则需克服管理、个性化和法规遵从等挑战,采用先进技术同时确保隐私和安全,以同理心驱动的策略建立客户连接,实现业务成功。
19 1
打造精准营销!营销电子邮件以客户为中心策略解析!
|
13天前
|
缓存 关系型数据库 MySQL
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
|
18天前
|
存储 NoSQL 算法
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(二)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
33 0
|
22天前
|
存储 JSON 安全
【C++ 泛型编程 综合篇】泛型编程深度解析:C++中的五种类型泛型策略综合对比
【C++ 泛型编程 综合篇】泛型编程深度解析:C++中的五种类型泛型策略综合对比
64 1
|
4天前
|
存储 缓存 自动驾驶
缓存策略与Apollo:优化网络请求性能
缓存策略与Apollo:优化网络请求性能
|
7天前
|
存储 缓存 iOS开发
基于iOS的高效图片缓存策略实现
【4月更文挑战第9天】在移动应用开发中,图片资源的加载与缓存是影响用户体验的重要因素之一。特别是对于iOS平台,合理设计图片缓存策略不仅能够提升用户浏览图片时的流畅度,还能有效降低应用程序的内存压力。本文将介绍一种针对iOS环境优化的图片缓存技术,该技术通过多级缓存机制和内存管理策略,实现了图片快速加载与低内存消耗的目标。我们将从系统架构、关键技术细节以及性能评估等方面展开讨论,为开发者提供一套实用的图片缓存解决方案。
12 0
|
11天前
|
负载均衡 算法 Linux
深度解析:Linux内核调度器的演变与优化策略
【4月更文挑战第5天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分——内核调度器。文章将首先回顾Linux内核调度器的发展历程,从早期的简单轮转调度(Round Robin)到现代的完全公平调度器(Completely Fair Scheduler, CFS)。接着,分析当前CFS面临的挑战以及社区提出的各种优化方案,最后提出未来可能的发展趋势和研究方向。通过本文,读者将对Linux调度器的原理、实现及其优化有一个全面的认识。
|
20天前
|
算法 IDE Linux
【CMake 小知识】CMake中的库目标命名和查找策略解析
【CMake 小知识】CMake中的库目标命名和查找策略解析
97 1

推荐镜像

更多