之前写过一篇文章,通过Android提供的AsyncTask和自己实现的ThreadPool两种方法来实现了图片数据的异步加载,但在实际应用中,仅仅做到这样是不够的。我们在GridView中加载了大量的图片数据,但当我们向上向下来回滚动的时候,之前加载过的图片都会重新从服务器中获取,这样显然不是很好的用户体验。对用户来说,在上下滚动的时候,曾经看过不久的图片能够马上显示出来,而不是要等待从服务器下载那么久,才是更好的用户体验。
为了实现这样的需求,Android为我们提供了LruCache和DiskLruCache两个工具。
本文原创,如需转载,请注明转载地址http://blog.csdn.net/carrey1989/article/details/12152651
我们今天讲解的代码建立在上一篇文章的基础之上,感兴趣的同学可以点击这里来查看。在文章最后有提供源码的下载链接。
首先整体来说一下我们的思路:
我们将在一个GridView中加载图片数据,在获取图片数据的时候,首先判断内存缓存中是否保存了这张图片。如果没有,将启动一个异步回调过程,先从SD卡中获得缓存的图片,如果依然没有,就会从服务器中来请求图片数据了。剩下的步骤就是刷新和缓存的工作了。
上面的思路比较笼统,接下来会比较详细的讲解具体的代码。
看一下项目的结构:
与内存缓存和SD卡缓存相关的处理主要在MainActivity.java和MyThreadPoolTask.java两个类中。
先看一下MainActivity.java的代码,下面会做出具体的解释:
package com.carrey.bitmapcachedemo; import java.io.File; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.HashMap; import com.carrey.customview.customview.CustomView; import android.os.Bundle; import android.os.Environment; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.GridView; /** * LruCache and DiskLruCache * @author carrey * */ public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private LayoutInflater inflater; private String webServerStr; private ThreadPoolManager poolManager; //LruCache private static final int MEM_MAX_SIZE = 4 * 1024 * 1024;// MEM 4MB private LruCache<String, Bitmap> mMemoryCache = null; //DiskLruCache private static final int DISK_MAX_SIZE = 32 * 1024 * 1024;// SD 32MB private DiskLruCache mDiskCacke = null; //下载任务队列 Map的key代表要下载的图片url,后面的List队列包含所有请求这张图片的回调 private HashMap<String, ArrayList<SoftReference<BitmapCallback>>> mCallbacks = new HashMap<String, ArrayList<SoftReference<BitmapCallback>>>(); private GridView gridView; private GridAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); webServerStr = getResources().getString(R.string.web_server); inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); poolManager = new ThreadPoolManager(ThreadPoolManager.TYPE_LIFO, 5); //内存缓存 mMemoryCache = new LruCache<String, Bitmap>(MEM_MAX_SIZE) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { // 不要在这里强制回收oldValue,因为从LruCache清掉的对象可能在屏幕上显示着, // 这样就会出现空白现象 super.entryRemoved(evicted, key, oldValue, newValue); } }; //SD卡缓存 File cacheDir = new File( Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "cacheDir"); mDiskCacke = DiskLruCache.openCache(cacheDir, DISK_MAX_SIZE); gridView = (GridView) findViewById(R.id.gridview); adapter = new GridAdapter(); gridView.setAdapter(adapter); } private class GridAdapter extends BaseAdapter { private Bitmap mBackgroundBitmap; public GridAdapter() { mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.item_bg); } @Override public int getCount() { return 99; } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { holder = new ViewHolder(); convertView = inflater.inflate(R.layout.item, null); holder.customView = (CustomView) convertView.findViewById(R.id.customview); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.customView.position = position; holder.customView.setBackgroundBitmap(mBackgroundBitmap); holder.customView.setTitleText("cache demo"); holder.customView.setSubTitleText("position: " + position); String imageURL = ImageHelper.getImageUrl(webServerStr, position); holder.customView.setUUID(imageURL); holder.customView.setImageBitmap(getBitmap(holder.customView.getUUID(), holder.customView.getBitmapCallback())); return convertView; } } static class ViewHolder { CustomView customView; } private Bitmap getBitmap(String url, BitmapCallback callback) { if (url == null) { return null; } synchronized (mMemoryCache) { Bitmap bitmap = mMemoryCache.get(url); if (bitmap != null && !bitmap.isRecycled()) { Log.i(TAG, "get bitmap from mem: url = " + url); return bitmap; } } //内存中没有,异步回调 if (callback != null) { ArrayList<SoftReference<BitmapCallback>> callbacks = null; synchronized (mCallbacks) { if ((callbacks = mCallbacks.get(url)) != null) { if (!callbacks.contains(callback)) { callbacks.add(new SoftReference<BitmapCallback>(callback)); } return null; } else { callbacks = new ArrayList<SoftReference<BitmapCallback>>(); callbacks.add(new SoftReference<BitmapCallback>(callback)); mCallbacks.put(url, callbacks); } } poolManager.start(); poolManager.addAsyncTask(new MyThreadPoolTask(url, mDiskCacke, mTaskCallback)); } return null; } private BitmapCallback mTaskCallback = new BitmapCallback() { @Override public void onReady(String key, Bitmap bitmap) { Log.i(TAG, "task done callback url = " + key); ArrayList<SoftReference<BitmapCallback>> callbacks = null; synchronized (mCallbacks) { if ((callbacks = mCallbacks.get(key)) != null) { mCallbacks.remove(key); } } if (bitmap != null) { synchronized (mDiskCacke) { if (!mDiskCacke.containsKey(key)) { Log.i(TAG, "put bitmap to SD url = " + key); mDiskCacke.put(key, bitmap); } } synchronized (mMemoryCache) { Bitmap bmp = mMemoryCache.get(key); if (bmp == null || bmp.isRecycled()) { mMemoryCache.put(key, bitmap); } } } //调用请求这张图片的回调 if (callbacks != null) { for (int i = 0; i < callbacks.size(); i++) { SoftReference<BitmapCallback> ref = callbacks.get(i); BitmapCallback cal = ref.get(); if (cal != null) { cal.onReady(key, bitmap); } } } } }; }在上面的代码中,我们对LruCache和DiskLruCache做了相关的初始化工作,设置了内存缓存的大小是4MB,SD卡缓存的大小是32MB。
需要特别解释的是这里定义了一个任务队列mCallbacks变量,这是一个HashMap,其中key的值是要下载的图片的url,value是一个ArrayList,在这个List中保存的是所有请求这个url的图片的视图的刷新回调对象。简单的理解就是,key表示一个特定的资源,value表示哪些家伙请求了这个资源。
我们在刷新视图的图片的时候,会先调用getBitmap方法,在其中先判断内存缓存中是否缓存了这张图片,如果有,就直接刷新,如果没有,就会向线程池中添加一个任务,启动一个异步刷新的过程,但是在这之前,会先对任务队列进行一些操作:我们会根据任务队列中的情况,判断当前是否有视图请求了这张图片,如果有,则再次判断当前请求的视图是否已经在队列之中。根据不同的情况,我们对队列进行不同的处理。
接下来就进入MyThreadPoolTask.java了,代码如下:
package com.carrey.bitmapcachedemo; import android.graphics.Bitmap; import android.os.Process; import android.util.Log; /** * 任务单元,在内存缓存没有图片的情况下从sd卡或者网络中获得图片 * 然后调用回调来进行下一步操作 * @author carrey * */ public class MyThreadPoolTask extends ThreadPoolTask { private static final String TAG = "MyThreadPoolTask"; private DiskLruCache mDiskLruCache; private BitmapCallback callback; public MyThreadPoolTask(String url, DiskLruCache mDiskLruCache, BitmapCallback callback) { super(url); this.mDiskLruCache = mDiskLruCache; this.callback = callback; } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST); Bitmap bitmap = null; synchronized (mDiskLruCache) { bitmap = mDiskLruCache.get(url); } if (bitmap == null) { Log.i(TAG, "bitmap from net, url = " + url); bitmap = ImageHelper.loadBitmapFromNet(url); } else { Log.i(TAG, "bitmap from SD, url = " + url); } if (callback != null) { callback.onReady(url, bitmap); } } }这里面的处理比较简单,我们先判断sd卡中是否有请求的资源,如果没有,将从网络中进行获取,最后调用MainActivity.java中实现的mTaskCallback回调对象,在回调的实现中,会依次更新内存缓存和SD卡缓存,然后依次循环请求该图片的队列,刷新他们的视图。
之所以做了这样一个任务队列的设计,是因为异步过程有很多的不确定性。
请求队列中视图的回调定义如下,在CustomView.java中:
private String uuid = null; public void setUUID(String uuid) { this.uuid = uuid; } public String getUUID() { return this.uuid; } public BitmapCallback mBitmapCallback = new BitmapCallback() { @Override public void onReady(String key, Bitmap bitmap) { if (bitmap != null && key != null && CustomView.this.uuid != null) { if (key.equals(CustomView.this.uuid)) { setImageBitmap(bitmap); postInvalidate(); } } } }; public BitmapCallback getBitmapCallback() { return this.mBitmapCallback; }
相比上一篇文章,我们添加了一个uuid属性,这也是因为convertView是不断复用的,如果一个convertView请求了一个网络资源,还没有加载,之后再次被复用,一旦第二次加载完成早于第一次加载,那么之后第一次加载的结果就会覆盖第二次加载,这样就造成了数据不准确,所以在这里需要一个标识作为判断,保证数据刷新的准确性。这里的uuid实际上是图片的url,所以如果两次请求的是同一张图片,这种情况是可以刷新两次的。
今天的代码依然用到了之前写过的一个自定义控件,如果对此感兴趣,可以点击这里来查看。
到这里关键的代码基本就讲解完了,具体实现的效果如下:
通过与上一篇文章的效果对比可以看出,在之前加载过的图片数据再次加载的速度上要快了不少。
如果我们这个时候退出应用,然后再次打开应用,这个时候加载的图片实际上都是从SD卡中加载的:
DiskLruCache会在SD卡中创建我们指定的缓存目录,在其中会存放我们缓存的文件:
下面贴出源码的下载链接,如果有什么问题,欢迎留言交流!
源码下载