Android 应用开发 之使用LruCache和DiskLruCache来在内存和SD卡中缓存图片

http://blog.csdn.net/carrey1989/article/details/12152651

 

  之前写过一篇文章,通过Android提供的AsyncTask和自己实现的ThreadPool两种方法来实现了图片数据的异步加载,但在实际应用中,仅仅做到这样是不够的。我们在GridView中加载了大量的图片数据,但当我们向上向下来回滚动的时候,之前加载过的图片都会重新从服务器中获取,这样显然不是很好的用户体验。对用户来说,在上下滚动的时候,曾经看过不久的图片能够马上显示出来,而不是要等待从服务器下载那么久,才是更好的用户体验。

    为了实现这样的需求,Android为我们提供了LruCache和DiskLruCache两个工具。

    本文原创,如需转载,请注明转载地址http://blog.csdn.net/carrey1989/article/details/12152651

    我们今天讲解的代码建立在上一篇文章的基础之上,感兴趣的同学可以点击这里来查看。在文章最后有提供源码的下载链接。

    首先整体来说一下我们的思路:

    我们将在一个GridView中加载图片数据,在获取图片数据的时候,首先判断内存缓存中是否保存了这张图片。如果没有,将启动一个异步回调过程,先从SD卡中获得缓存的图片,如果依然没有,就会从服务器中来请求图片数据了。剩下的步骤就是刷新和缓存的工作了。

    上面的思路比较笼统,接下来会比较详细的讲解具体的代码。

    看一下项目的结构:

    与内存缓存和SD卡缓存相关的处理主要在MainActivity.java和MyThreadPoolTask.java两个类中。

    先看一下MainActivity.java的代码,下面会做出具体的解释:

  1. package com.carrey.bitmapcachedemo;
  2. import java.io.File;
  3. import java.lang.ref.SoftReference;
  4. import java.util.ArrayList;
  5. import java.util.HashMap;
  6. import com.carrey.customview.customview.CustomView;
  7. import android.os.Bundle;
  8. import android.os.Environment;
  9. import android.app.Activity;
  10. import android.content.Context;
  11. import android.graphics.Bitmap;
  12. import android.graphics.BitmapFactory;
  13. import android.util.Log;
  14. import android.view.LayoutInflater;
  15. import android.view.View;
  16. import android.view.ViewGroup;
  17. import android.widget.BaseAdapter;
  18. import android.widget.GridView;
  19. /**
  20.  * LruCache and DiskLruCache
  21.  * @author carrey
  22.  *
  23.  */
  24. public class MainActivity extends Activity {
  25.     private static final String TAG = “MainActivity”;
  26.     private LayoutInflater inflater;
  27.     private String webServerStr;
  28.     private ThreadPoolManager poolManager;
  29.     //LruCache
  30.     private static final int MEM_MAX_SIZE = 4 * 1024 * 1024;// MEM 4MB
  31.     private LruCache<String, Bitmap> mMemoryCache = null;
  32.     //DiskLruCache
  33.     private static final int DISK_MAX_SIZE = 32 * 1024 * 1024;// SD 32MB
  34.     private DiskLruCache mDiskCacke = null;
  35.     //下载任务队列 Map的key代表要下载的图片url,后面的List队列包含所有请求这张图片的回调
  36.     private HashMap<String, ArrayList<SoftReference<BitmapCallback>>> mCallbacks =
  37.             new HashMap<String, ArrayList<SoftReference<BitmapCallback>>>();
  38.     private GridView gridView;
  39.     private GridAdapter adapter;
  40.     @Override
  41.     protected void onCreate(Bundle savedInstanceState) {
  42.         super.onCreate(savedInstanceState);
  43.         setContentView(R.layout.activity_main);
  44.         webServerStr = getResources().getString(R.string.web_server);
  45.         inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  46.         poolManager = new ThreadPoolManager(ThreadPoolManager.TYPE_LIFO, 5);
  47.         //内存缓存
  48.         mMemoryCache = new LruCache<String, Bitmap>(MEM_MAX_SIZE) {
  49.             @Override
  50.             protected int sizeOf(String key, Bitmap value) {
  51.                 return value.getRowBytes() * value.getHeight();
  52.             }
  53.             @Override
  54.             protected void entryRemoved(boolean evicted, String key,
  55.                     Bitmap oldValue, Bitmap newValue) {
  56.                 // 不要在这里强制回收oldValue,因为从LruCache清掉的对象可能在屏幕上显示着,
  57.                 // 这样就会出现空白现象
  58.                 super.entryRemoved(evicted, key, oldValue, newValue);
  59.             }
  60.         };
  61.         //SD卡缓存
  62.         File cacheDir = new File(
  63.                 Environment.getExternalStorageDirectory().getAbsolutePath() +
  64.                 File.separator +
  65.                 “cacheDir”);
  66.         mDiskCacke = DiskLruCache.openCache(cacheDir, DISK_MAX_SIZE);
  67.         gridView = (GridView) findViewById(R.id.gridview);
  68.         adapter = new GridAdapter();
  69.         gridView.setAdapter(adapter);
  70.     }
  71.     private class GridAdapter extends BaseAdapter {
  72.         private Bitmap mBackgroundBitmap;
  73.         public GridAdapter() {
  74.             mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.item_bg);
  75.         }
  76.         @Override
  77.         public int getCount() {
  78.             return 99;
  79.         }
  80.         @Override
  81.         public Object getItem(int position) {
  82.             return null;
  83.         }
  84.         @Override
  85.         public long getItemId(int position) {
  86.             return 0;
  87.         }
  88.         @Override
  89.         public View getView(int position, View convertView, ViewGroup parent) {
  90.             ViewHolder holder = null;
  91.             if (convertView == null) {
  92.                 holder = new ViewHolder();
  93.                 convertView = inflater.inflate(R.layout.item, null);
  94.                 holder.customView = (CustomView) convertView.findViewById(R.id.customview);
  95.                 convertView.setTag(holder);
  96.             } else {
  97.                 holder = (ViewHolder) convertView.getTag();
  98.             }
  99.             holder.customView.position = position;
  100.             holder.customView.setBackgroundBitmap(mBackgroundBitmap);
  101.             holder.customView.setTitleText(“cache demo”);
  102.             holder.customView.setSubTitleText(“position: “ + position);
  103.             String imageURL = ImageHelper.getImageUrl(webServerStr, position);
  104.             holder.customView.setUUID(imageURL);
  105.             holder.customView.setImageBitmap(getBitmap(holder.customView.getUUID(), holder.customView.getBitmapCallback()));
  106.             return convertView;
  107.         }
  108.     }
  109.     static class ViewHolder {
  110.         CustomView customView;
  111.     }
  112.     private Bitmap getBitmap(String url, BitmapCallback callback) {
  113.         if (url == null) {
  114.             return null;
  115.         }
  116.         synchronized (mMemoryCache) {
  117.             Bitmap bitmap = mMemoryCache.get(url);
  118.             if (bitmap != null && !bitmap.isRecycled()) {
  119.                 Log.i(TAG, “get bitmap from mem: url = “ + url);
  120.                 return bitmap;
  121.             }
  122.         }
  123.         //内存中没有,异步回调
  124.         if (callback != null) {
  125.             ArrayList<SoftReference<BitmapCallback>> callbacks = null;
  126.             synchronized (mCallbacks) {
  127.                 if ((callbacks = mCallbacks.get(url)) != null) {
  128.                     if (!callbacks.contains(callback)) {
  129.                         callbacks.add(new SoftReference<BitmapCallback>(callback));
  130.                     }
  131.                     return null;
  132.                 } else {
  133.                     callbacks = new ArrayList<SoftReference<BitmapCallback>>();
  134.                     callbacks.add(new SoftReference<BitmapCallback>(callback));
  135.                     mCallbacks.put(url, callbacks);
  136.                 }
  137.             }
  138.             poolManager.start();
  139.             poolManager.addAsyncTask(new MyThreadPoolTask(url, mDiskCacke, mTaskCallback));
  140.         }
  141.         return null;
  142.     }
  143.     private BitmapCallback mTaskCallback = new BitmapCallback() {
  144.         @Override
  145.         public void onReady(String key, Bitmap bitmap) {
  146.             Log.i(TAG, “task done callback url = “ + key);
  147.             ArrayList<SoftReference<BitmapCallback>> callbacks = null;
  148.             synchronized (mCallbacks) {
  149.                 if ((callbacks = mCallbacks.get(key)) != null) {
  150.                     mCallbacks.remove(key);
  151.                 }
  152.             }
  153.             if (bitmap != null) {
  154.                 synchronized (mDiskCacke) {
  155.                     if (!mDiskCacke.containsKey(key)) {
  156.                         Log.i(TAG, “put bitmap to SD url = “ + key);
  157.                         mDiskCacke.put(key, bitmap);
  158.                     }
  159.                 }
  160.                 synchronized (mMemoryCache) {
  161.                     Bitmap bmp = mMemoryCache.get(key);
  162.                     if (bmp == null || bmp.isRecycled()) {
  163.                         mMemoryCache.put(key, bitmap);
  164.                     }
  165.                 }
  166.             }
  167.             //调用请求这张图片的回调
  168.             if (callbacks != null) {
  169.                 for (int i = 0; i < callbacks.size(); i++) {
  170.                     SoftReference<BitmapCallback> ref = callbacks.get(i);
  171.                     BitmapCallback cal = ref.get();
  172.                     if (cal != null) {
  173.                         cal.onReady(key, bitmap);
  174.                     }
  175.                 }
  176.             }
  177.         }
  178.     };
  179. }

    在上面的代码中,我们对LruCache和DiskLruCache做了相关的初始化工作,设置了内存缓存的大小是4MB,SD卡缓存的大小是32MB。

    需要特别解释的是这里定义了一个任务队列mCallbacks变量,这是一个HashMap,其中key的值是要下载的图片的url,value是一个ArrayList,在这个List中保存的是所有请求这个url的图片的视图的刷新回调对象。简单的理解就是,key表示一个特定的资源,value表示哪些家伙请求了这个资源。

    我们在刷新视图的图片的时候,会先调用getBitmap方法,在其中先判断内存缓存中是否缓存了这张图片,如果有,就直接刷新,如果没有,就会向线程池中添加一个任务,启动一个异步刷新的过程,但是在这之前,会先对任务队列进行一些操作:我们会根据任务队列中的情况,判断当前是否有视图请求了这张图片,如果有,则再次判断当前请求的视图是否已经在队列之中。根据不同的情况,我们对队列进行不同的处理。

    接下来就进入MyThreadPoolTask.java了,代码如下:

  1. package com.carrey.bitmapcachedemo;
  2. import android.graphics.Bitmap;
  3. import android.os.Process;
  4. import android.util.Log;
  5. /**
  6.  * 任务单元,在内存缓存没有图片的情况下从sd卡或者网络中获得图片
  7.  * 然后调用回调来进行下一步操作
  8.  * @author carrey
  9.  *
  10.  */
  11. public class MyThreadPoolTask extends ThreadPoolTask {
  12.     private static final String TAG = “MyThreadPoolTask”;
  13.     private DiskLruCache mDiskLruCache;
  14.     private BitmapCallback callback;
  15.     public MyThreadPoolTask(String url, DiskLruCache mDiskLruCache, BitmapCallback callback) {
  16.         super(url);
  17.         this.mDiskLruCache = mDiskLruCache;
  18.         this.callback = callback;
  19.     }
  20.     @Override
  21.     public void run() {
  22.         Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
  23.         Bitmap bitmap = null;
  24.         synchronized (mDiskLruCache) {
  25.             bitmap = mDiskLruCache.get(url);
  26.         }
  27.         if (bitmap == null) {
  28.             Log.i(TAG, “bitmap from net, url = “ + url);
  29.             bitmap = ImageHelper.loadBitmapFromNet(url);
  30.         } else {
  31.             Log.i(TAG, “bitmap from SD, url = “ + url);
  32.         }
  33.         if (callback != null) {
  34.             callback.onReady(url, bitmap);
  35.         }
  36.     }
  37. }

    这里面的处理比较简单,我们先判断sd卡中是否有请求的资源,如果没有,将从网络中进行获取,最后调用MainActivity.java中实现的mTaskCallback回调对象,在回调的实现中,会依次更新内存缓存和SD卡缓存,然后依次循环请求该图片的队列,刷新他们的视图。

    之所以做了这样一个任务队列的设计,是因为异步过程有很多的不确定性。

    请求队列中视图的回调定义如下,在CustomView.java中:

  1. private String uuid = null;
  2. public void setUUID(String uuid) {
  3.     this.uuid = uuid;
  4. }
  5. public String getUUID() {
  6.     return this.uuid;
  7. }
  8. public BitmapCallback mBitmapCallback = new BitmapCallback() {
  9.     @Override
  10.     public void onReady(String key, Bitmap bitmap) {
  11.         if (bitmap != null && key != null && CustomView.this.uuid != null) {
  12.             if (key.equals(CustomView.this.uuid)) {
  13.                 setImageBitmap(bitmap);
  14.                 postInvalidate();
  15.             }
  16.         }
  17.     }
  18. };
  19. public BitmapCallback getBitmapCallback() {
  20.     return this.mBitmapCallback;
  21. }

    相比上一篇文章,我们添加了一个uuid属性,这也是因为convertView是不断复用的,如果一个convertView请求了一个网络资源,还没有加载,之后再次被复用,一旦第二次加载完成早于第一次加载,那么之后第一次加载的结果就会覆盖第二次加载,这样就造成了数据不准确,所以在这里需要一个标识作为判断,保证数据刷新的准确性。这里的uuid实际上是图片的url,所以如果两次请求的是同一张图片,这种情况是可以刷新两次的。

    今天的代码依然用到了之前写过的一个自定义控件,如果对此感兴趣,可以点击这里来查看。

    到这里关键的代码基本就讲解完了,具体实现的效果如下:

    通过与上一篇文章的效果对比可以看出,在之前加载过的图片数据再次加载的速度上要快了不少。
如果我们这个时候退出应用,然后再次打开应用,这个时候加载的图片实际上都是从SD卡中加载的:

    DiskLruCache会在SD卡中创建我们指定的缓存目录,在其中会存放我们缓存的文件:

    下面贴出源码的下载链接,如果有什么问题,欢迎留言交流!

源码下载

版权声明:本文为博主原创文章,未经博主允许不得转载。