Android Volley的HTTP缓存策略

前言:

HTTP缓存策略,可以有效的减少重复请求,降低服务器后台压力,和减少客户端的流量带宽。

不了解HTTP缓存策略,建议先阅读Android HTTP 缓存策略(用于检查磁盘数据是否过期)

服务器端返回的HTTP缓存策略

找到NetworkDispatcher类:查看执行HTTP请求后的解析,缓存操作。

public class NetworkDispatcher extends Thread {

    @Override
    public void run() {
       //....省略部分代码

       //在网络线程中指向解析响应的数据
       Response<?> response = request.parseNetworkResponse(networkResponse);

       //若是需要缓存,则将解析后数据写入缓存中。
       //注意点:在304s情况下(即内容数据相同时),只会更新缓存的metadata, 不会更新内容数据。
        if (request.shouldCache() && response.cacheEntry != null) {
             mCache.put(request.getCacheKey(), response.cacheEntry);
        }      
    }  
}

从以上代码可以知道,执行Request的parseNetworkResponse()解析网络数据,生成一个Cache.Entry,然后写入磁盘中。

先来了解Request的parseNetworkResponse().

这里,找到一个Request的子类StringRequest:

  @Override
  protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

以上代码,是解析成指定返回类型的数据,和解析header。

找到HttpHeaderParser类,查看解析Header的方法:

public class HttpHeaderParser {

    /**
     * 从NetworkRespone 生成一个Cache.Entry,即将http缓存机制信息写入Entry中
     */
    public static Cache.Entry parseCacheHeaders(NetworkResponse response) {

        //当前时间
        long now = System.currentTimeMillis();
        //服务器返回的标头
        Map<String, String> headers = response.headers;

        long serverDate = 0;
        long serverExpires = 0;
        long softExpire = 0;
        long maxAge = 0;
        boolean hasCacheControl = false;

        String serverEtag = null;
        String headerValue;
        //服务器响应的时间
        headerValue = headers.get("Date");
        if (headerValue != null) {
            //获取到long类型的服务器响应时间
            serverDate = parseDateAsEpoch(headerValue);
        }
        headerValue = headers.get("Cache-Control");
        if (headerValue != null) {
            hasCacheControl = true;
            String[] tokens = headerValue.split(",");
            for (int i = 0; i < tokens.length; i++) {
                String token = tokens[i].trim();
                if (token.equals("no-cache") || token.equals("no-store")) {
                   //数据处于安全问题,不缓存磁盘
                    return null;
                } else if (token.startsWith("max-age=")) {
                    try {
                        maxAge = Long.parseLong(token.substring(8));
                    } catch (Exception e) {
                    }
                } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                    maxAge = 0;
                }
            }
        }
        //服务器返回的数据过期
        headerValue = headers.get("Expires");
        if (headerValue != null) {
            //服务器返回的数据过期时间
            serverExpires = parseDateAsEpoch(headerValue);
        }
        serverEtag = headers.get("ETag");
        //Cache-Control优先于Expire
        if (hasCacheControl) {  //Cache-Control标头存在的情况
            //过期时间=(当前时间+缓存的有效时间*1000)
            softExpire = now + maxAge * 1000;
        } else if (serverDate > 0 && serverExpires >= serverDate) { //Cache-Control标头不存在的情况
            //在Http规范中Expire标头的语义是softExpire.
            //过期时间=现在时间+(服务器返回数据的过期时间-服务器响应时间)
            softExpire = now + (serverExpires - serverDate);
        }

        Cache.Entry entry = new Cache.Entry();
        //Response中body信息
        entry.data = response.data;
        // eTag
        entry.etag = serverEtag;
        //缓存时间
        entry.softTtl = softExpire;
        //过期时间
        entry.ttl = entry.softTtl;
        //服务器响应时间
        entry.serverDate = serverDate;
        //Response中全部Header信息
        entry.responseHeaders = headers;

        return entry;
    }
    /**
     * 按RFC1123格式解析时间,返回long类型的date
     */
    public static long parseDateAsEpoch(String dateStr) {
        try {
            DateUtils.parseDate(dateStr).getTime();
        } catch (DateParseException e) {
            return 0;
        }
    }

}

从以上代码可知,解析HTTP缓存策略中的过期时间,缓存时间,etag,服务器响应时间等等。

找到Cache.Entry类: 用于保存Response数据

    public static class Entry {
        /**   从缓存中的数据 */
        public byte[] data;
        /** ETag 用于缓存一致性.   */
        public String etag;
        /**  从服务器响应的时间 */
        public long serverDate;
        /** TTL 过期时间  */
        public long ttl;
        /** Soft TTL 缓存时间  */
        public long softTtl;
        /**  从服务器上响应的header,默认不为空  */
        public Map<String, String> responseHeaders = Collections.emptyMap();
        /**
         * 返回true,实体已经过期,必须重新获取。
         **/
        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }
        /**
         *  返回true,数据不在缓存期间,任可以展示,但需要从原始数据源中刷新。
         * */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

用于判断是否请求最新数据的判断情况

  • 当前时间大于过期时间,需要重新从服务器获取新数据。
  • 当前时间小于过期时间,且当前时间大于过期时间,可以加载缓存数据,但必须去请求最新的数据,最新数据可能发生改变。
  • 当前时间小于缓存时间,本地数据可以直接使用。
检查HTTP缓存策略,是否在缓存期间,是否过期

找到CacheDispatcher类:

public class CacheDispatcher extends Thread {
    @Override
    public void run() {

         //从磁盘中获取该请求需要的数据,若是没有则加入网络队列中,执行网络操作。
         Cache.Entry entry = mCache.get(request.getCacheKey());
         if (entry == null) {
                request.addMarker("cache-miss");
                //磁盘中无缓存数据,添加到网络请求中,执行网络获取数据
                mNetworkQueue.put(request);
                continue;
         }
         //若是这缓存数据已经过期,则进行网络获取新数据
         if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                //确保缓存一致性,使用从缓存中读取到的entry.
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
         }
         //解析从磁盘中读取到数据
         Response<?> response = request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders));
         //数据是否需要刷新
         if (!entry.refreshNeeded()) {
            //数据还在缓存期间中,响应动作在ExecutorDelivery中,将结果回调到主线程中
             mDelivery.postResponse(request, response);
         } else {
                /**
                 * 缓存数据过了缓存时间,但没有过期,则先回调加载磁盘数据
                 * ,然后执行网络操作,刷新最新数据,二次回调响应。
                 */
                 request.setCacheEntry(entry);
                  //标记响应是中间,还会继续刷新
                  response.intermediate = true;
                  //传递一个中间的响应回调到监听器中,同时也会传递一个请求去执行网络工作。
                 mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                            }
                        }
                    });
     }
}

从以上代码可知:

  • 先从缓存队列中获取到请求
  • 从磁盘中获取数据,若是为空,将该请求加入网络队列中,执行网络操作。
  • 反之,获取缓存数据,若是过期,则将该请求加入网络队列中,执行网络操作。
  • 反之,获取的缓存数据没有过期,若是该数据还在缓存期间,则传递响应数据。
  • 反之,该数据不在缓存期间,则先传递响应数据,且开启网络线程去刷新,二次传递新数据。
客户端的HTTP缓存策略

找到BasicNetwork类:该类执行网络线程中,用于操作HTTP请求

public class BasicNetwork implements Network {
         //....省略部分代码
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
                // 创一个Map来,存储上一次相同缓存key所对应请求的标头
                Map<String, String> headers = new HashMap<String, String>();
                //因磁盘中缓存的数据,已经过期,需要重新执行网络数据,而添加磁盘中缓存数据的标头
                addCacheHeaders(headers, request.getCacheEntry());
                //在HurlStack 中,执行HttpURLConnection,返回响应数据
                httpResponse = mHttpStack.performRequest(request, headers);

                //....省略部分代码
    }
     /**
     * 添加磁盘中缓存数据的一些标头,若是没有缓存,不需要任何操作。
     * <p>
     * 1. If-None-Match
     * 2. If-Modified-Since
     */
    private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
        //
        if (entry == null) {
            return;
        }
        if (entry.etag != null) {//添加是否匹配的标头
            headers.put("If-None-Match", entry.etag);
        }
        if (entry.serverDate > 0) {//
            Date refTime = new Date(entry.serverDate);
            headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
        }
    }
}

从以上代码,可知将本地HTTP缓存策略的表头信息发送给服务器。


实战案例

自定义一个3分钟缓存,3小时过期的圆形图片的请求CircleBitmapImageRequest。

public class CircleBitmapImageRequest extends ImageRequest {
    private static final android.graphics.Bitmap.Config bitmapConfig = Bitmap.Config.RGB_565;
    private static final Handler mainHandler = new Handler(Looper.getMainLooper());
    private  final BitmapResultListener resultListener;
    public CircleBitmapImageRequest(String url, ImageView imageView, BitmapResultListener resultListener) {
        this(url, imageView.getWidth(), imageView.getHeight(), bitmapConfig, resultListener);
    }
    public CircleBitmapImageRequest(String url, int maxWidth, int maxHeight, BitmapResultListener resultListener) {
        this(url, maxWidth, maxHeight, bitmapConfig, resultListener);
    }
    public CircleBitmapImageRequest(String url, int maxWidth, int maxHeight, Bitmap.Config decodeConfig, BitmapResultListener resultListener) {
        super(url, resultListener, maxWidth, maxHeight, decodeConfig, resultListener);
        this.resultListener = resultListener;
        loadPreviewBitmap();
    }
    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        Response<Bitmap> bitmapResponse=  super.parseNetworkResponse(response);
        if (bitmapResponse.isSuccess()){
            //重新添加具备缓存期间和过期时间的header
            bitmapResponse =Response.success(bitmapResponse.result,HttpResponseHeaderParser.parseSpecifiedTimeCacheHeaders(response));
        }
        return bitmapResponse;
    }

    @Override
    protected void deliverResponse(Bitmap response) {
        // circle crop bitmap
        Bitmap circleBitmap = BitmapUtils.circleCrop(response);
        if (resultListener!=null){
            resultListener.onResponse(circleBitmap);
        }
    }
    private void loadPreviewBitmap() {
        if (resultListener != null) {
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    resultListener.loadPreviewBitmap();
                }
            });
        }
    }

}

重写缓存策略:

public class HttpResponseHeaderParser extends HttpHeaderParser {
    /**
     * 忽视`Cache-control`表头,默认指定缓存时间,SoftTtl == 3 mins, ttl == 24 hours
     *
     * @param response
     * @return
     */
    public static Cache.Entry parseSpecifiedTimeCacheHeaders(NetworkResponse response) {
        Cache.Entry entry = parseCacheHeaders(response);
        long now = System.currentTimeMillis();
        //3分钟内,会缓存中,过完会刷新
        final long cacheHitButRefreshed = 3 * 60 * 1000;
        // in 12 hours this cache entry expires completely
        final long cacheExpired = 3 * 60 * 60 * 1000;
        //截止的缓存时间
        final long softExpire = now + cacheHitButRefreshed;
        //过期时间
        final long ttl = now + cacheExpired;
        entry.softTtl = softExpire;
        entry.ttl = ttl;
        return entry;
    }
}

资源参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值