之前学习volley框架,用ImageLoader可以设置内存缓存,用一个LruCache,就可以避免OOM且图片读取速度快,爽极了。
后来想,如果只是内存缓存的话,那退出程序或者内存不够大了,缓存的图片不就被清理掉了,这样每次启动程序就又得去网上下载图片,流量好贵的。
看起来还行,运行起来,效果怎么一般般,感觉卡卡滴,不对呀,难道磁盘缓存这么慢?于是把磁盘缓存先去掉,只留下内存缓存。试了一下,流畅了不少。不过奇怪的是,这样退出程序后再进来,图片依然还在!!!
我把刚才磁盘上的缓存文件在手机上删除后,断网,再进入程序,奇怪的事情再一次发生,图片依旧可以快速加载出来!!!
WTF,见鬼了。
难道LruCache不止可以内存缓存,还可以磁盘缓存?(不对呀,LruCache不是存在LinkedHashMap里的吗,怎么可能)赶紧打开LruCache源码瞧瞧,看了半天什么也没发现。
又想难道是Volley还有磁盘缓存???
为了证实这两个想法哪个正确,我又做了一个实验,把图片缓存进Lrucache里和 不设置任何缓存的 Volley里,结果是:
1:Lrucache只把图片存在内存里,退出程序内存里的图片就回收掉。
2:Volley不设置缓存,退出程序关掉网络,再次进入也能加载刚才下载下来的图片。
那么问题来了,我搞了半天的磁盘缓存原来volley的默认实现就有了,真是学艺不精啊~ 这下我学乖了,看源码,只有源码才靠得住,其他的坑太多。
2. 把图片保存在DiskBasedCache里,是在NetworkDispatcher的run()方法里:
DiskBasedCache里的put方法就是通过流把数据写到磁盘上的:
3. 把图片从DiskBasedCache里取出来,是在CacheDispatcher的run()里 :
DiskBasedCache的get是通过输入流把文件读取进来,然后转成CacheEntry的:
总结,Volley已经默认使用了磁盘缓存,官方推荐开发人员自己加上内存缓存,所以只需要很简单的在ImageLoader里设置内存缓存就可以实现二级缓存,不必配合上DiskLruCache了,配合DiskLruCache只会造成冗余,两次硬盘缓存,当然更慢了。
另外,开源框架用前最好过一下源码,不要像我这样搞了半天白忙活~~~~~
后来想,如果只是内存缓存的话,那退出程序或者内存不够大了,缓存的图片不就被清理掉了,这样每次启动程序就又得去网上下载图片,流量好贵的。
于是找到了磁盘缓存框架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;
- 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);
- }
- }
- }
- 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());
- }
- }
- 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;
- }
- }
- }
- 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 = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
- return entry.toCacheEntry(data);
- } catch (IOException e) {
- VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
- remove(key);
- return null;
- } finally {
- if (cis != null) {
- try {
- cis.close();
- } catch (IOException ioe) {
- return null;
- }
- }
- }
- }
总结,Volley已经默认使用了磁盘缓存,官方推荐开发人员自己加上内存缓存,所以只需要很简单的在ImageLoader里设置内存缓存就可以实现二级缓存,不必配合上DiskLruCache了,配合DiskLruCache只会造成冗余,两次硬盘缓存,当然更慢了。
另外,开源框架用前最好过一下源码,不要像我这样搞了半天白忙活~~~~~