首页 > 代码库 > 请注意,Volley已默认使用磁盘缓存

请注意,Volley已默认使用磁盘缓存

之前学习volley框架,用ImageLoader可以设置内存缓存,用一个LruCache,就可以避免OOM且图片读取速度快,爽极了。

后来想,如果只是内存缓存的话,那退出程序或者内存不够大了,缓存的图片不就被清理掉了,这样每次启动程序就又得去网上下载图片,流量好贵的。

于是找到了磁盘缓存框架DiskLruCache,这是一个挺著名的开源框架,网易云阅读等APP之前都用它来缓存图片,关于这个框架的使用可以看这篇博客。

找到这个框架后我就着手把DiskLruCache和Volley结合起来,用LruCache做一级缓存,用DiskLruCache做二级缓存,这样做使得程序又快又不用每次都去网上下载图片浪费流量。说干咋就干,做前股沟了一下,找到了一篇关于此实现的相关介绍,原来有老外也想到了这点(介绍链接),并把他的代码再github上开源出来。(github地址)

话不多说,down下来后看了这哥们的源码,并不复杂,只不过他并没有实现我心中的二级缓存,而是用BitmapLruImageCache和DiskLruImageCache将两个缓存分开,用的时候在ImageCacheManager里设置用哪个缓存。看来还是得自己动手,不一会儿就把它们揉合在一起了,源码如下:

public class LevelTwoCache implements ImageCache {

    private BitmapLruImageCache bitImageCache;

    private DiskLruImageCache diskImageCache;

    public LevelTwoCache(Context context, String uniqueName, int diskCacheSize, int memCacheSize,
            CompressFormat compressFormat, int quality) {

        bitImageCache = new BitmapLruImageCache(memCacheSize);
        diskImageCache = new DiskLruImageCache(context, uniqueName, diskCacheSize, compressFormat,
                quality);

    }

    /**
     * 先从内存获取图片,如果内存找不到就从磁盘里找,找到了存在内存里并返回
     */
    @Override
    public Bitmap getBitmap(String url) {
        String key = createKey(url);
        Bitmap bitmap = null;

        if (bitImageCache.getBitmap(key) == null) {
            if (diskImageCache.containsKey(key)) {
                return null;
            } else {
                bitmap = diskImageCache.getBitmap(key);
                bitImageCache.putBitmap(key, bitmap);
            }
        } else {
            bitmap = bitImageCache.getBitmap(key);
        }

        return bitmap;
    }

    /**
     * 首次图片从网络下载下来后分别保存在内存和磁盘缓存里
     */
    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        String key = createKey(url);

        bitImageCache.putBitmap(key, bitmap);
        diskImageCache.putBitmap(key, bitmap);
    }

    /**
     * 把url转成MD5
     */
    private String createKey(String url) {
        return getBitmapMDKey(url);
    }

    public String getBitmapMDKey(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    /**
     * 清除内存缓存
     */
    public void cleanMemCache() {
        bitImageCache.evictAll();
    }

    /**
     * 清除磁盘缓存
     */
    public void cleanDiskCache() {
        diskImageCache.clearCache();
    }

}

看起来还行,运行起来,效果怎么一般般,感觉卡卡滴,不对呀,难道磁盘缓存这么慢?于是把磁盘缓存先去掉,只留下内存缓存。试了一下,流畅了不少。不过奇怪的是,这样退出程序后再进来,图片依然还在!!!

我把刚才磁盘上的缓存文件在手机上删除后,断网,再进入程序,奇怪的事情再一次发生,图片依旧可以快速加载出来!!!

WTF,见鬼了。

难道LruCache不止可以内存缓存,还可以磁盘缓存?(不对呀,LruCache不是存在LinkedHashMap里的吗,怎么可能)赶紧打开LruCache源码瞧瞧,看了半天什么也没发现。

又想难道是Volley还有磁盘缓存???

为了证实这两个想法哪个正确,我又做了一个实验,把图片缓存进Lrucache里和不设置任何缓存的Volley里,结果是:

 1:Lrucache只把图片存在内存里,退出程序内存里的图片就回收掉。
 2:Volley不设置缓存,退出程序关掉网络,再次进入也能加载刚才下载下来的图片。

 那么问题来了,我搞了半天的磁盘缓存原来volley的默认实现就有了,真是学艺不精啊~ 这下我学乖了,看源码,只有源码才靠得住,其他的坑太多。

volley的源码分析,这里就不讲了,如有兴趣自己看或者结合源码看这篇博客,这里只看跟磁盘缓存相关的几段关键性代码,其中最重要的是DiskBasedCache类(里面的缓存算法也是LRU算法):

1:磁盘缓存的创建,在Volley.java的newRequestQueue方法里,随着requesQueue一起初始化 :

public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

        String userAgent = "volley/0";
        try {
            String packageName = context.getPackageName();
            PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
            userAgent = packageName + "/" + info.versionCode;
        } catch (NameNotFoundException e) {
        }

        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);
        
        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
        				 // 如果你不设置磁盘缓存最大值的话这里初始化(默认是5M)
        	queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
        				 // 设置了最大缓存值在这里初始化
        	queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        queue.start();

        return queue;
    
2. 把图片保存在DiskBasedCache里,是在NetworkDispatcher的run()方法里:

    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            Request<?> request;
            try {
                // Take a request from the queue.
                request = mQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }

            try {
                request.addMarker("network-queue-take");

                // If the request was cancelled already, do not perform the
                // network request.
                if (request.isCanceled()) {
                    request.finish("network-discard-cancelled");
                    continue;
                }

                addTrafficStatsTag(request);

                // Perform the network request.
                NetworkResponse networkResponse = mNetwork.performRequest(request);
                request.addMarker("network-http-complete");

                // If the server returned 304 AND we delivered a response already,
                // we're done -- don't deliver a second identical response.
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                    request.finish("not-modified");
                    continue;
                }

                // Parse the response here on the worker thread.
                Response<?> response = request.parseNetworkResponse(networkResponse);
                request.addMarker("network-parse-complete");

                
                //默认需要缓存,把response.cacheEntry保存到DiskBasedCache
                if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

                // Post the response back.
                request.markDelivered();
                mDelivery.postResponse(request, response);
            } catch (VolleyError volleyError) {
                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                parseAndDeliverNetworkError(request, volleyError);
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
                VolleyError volleyError = new VolleyError(e);
                volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
                mDelivery.postError(request, volleyError);
            }
        }
    }
DiskBasedCache里的put方法就是通过流把数据写到磁盘上的:

public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }
3. 把图片从DiskBasedCache里取出来,是在CacheDispatcher的run()里 :

public void run() {
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // Make a blocking call to initialize the cache.
        mCache.initialize();

        while (true) {
            try {
                // Get a request from the cache triage queue, blocking until
                // at least one is available.
                final Request<?> request = mCacheQueue.take();
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                //从DiskBasedCache取出bitmap数据
                Cache.Entry entry = mCache.get(request.getCacheKey());

                if (entry == null) {
                    request.addMarker("cache-miss");
                    // Cache miss; send off to the network dispatcher.
                    mNetworkQueue.put(request);
                    continue;
                }

                // If it is completely expired, just send it to the network.
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // We have a cache hit; parse its data for delivery back to the request.
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // Completely unexpired cache hit. Just deliver the response.
                    mDelivery.postResponse(request, response);
                } else {
                    // Soft-expired cache hit. We can deliver the cached response,
                    // but we need to also send the request to the network for
                    // refreshing.
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }

            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
        }
    }
DiskBasedCache的get是通过输入流把文件读取进来,然后转成CacheEntry的:

public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }

        File file = getFileForKey(key);
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new FileInputStream(file));
            CacheHeader.readHeader(cis); // eat header
            byte[] data = http://www.mamicode.com/streamToBytes(cis, (int) (file.length() - cis.bytesRead));>
总结,Volley已经默认使用了磁盘缓存,官方推荐开发人员自己加上内存缓存,所以只需要很简单的在ImageLoader里设置内存缓存就可以实现二级缓存,不必配合上DiskLruCache了,配合DiskLruCache只会造成冗余,两次硬盘缓存,当然更慢了。

另外,开源框架用前最好过一下源码,不要像我这样搞了半天白忙活~~~~~

请注意,Volley已默认使用磁盘缓存