之前写过一篇文章,通过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;//LruCacheprivate static final int MEM_MAX_SIZE = 4 * 1024 * 1024;// MEM 4MBprivate LruCache<String, Bitmap> mMemoryCache = null;//DiskLruCacheprivate static final int DISK_MAX_SIZE = 32 * 1024 * 1024;// SD 32MBprivate 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;@Overrideprotected 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) {@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getRowBytes() * value.getHeight();}@Overrideprotected 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);}@Overridepublic int getCount() {return 99;}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic 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() {@Overridepublic 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;}@Overridepublic 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() {@Overridepublic 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卡中创建我们指定的缓存目录,在其中会存放我们缓存的文件:

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

源码下载

更多相关文章

  1. Android添加图片水印
  2. Android(安卓)10适配要点,作用域存储
  3. Loading Large Bitmaps Efficiently 有效的加载大图片
  4. android 仿 ios 搜索界面跳转效果
  5. 那些Android上的性能优化
  6. android studio 适配android7.0 android 6.0拍照调用系统裁剪工
  7. android 自定义ScrollView实现背景图片伸缩的实现代码及思路
  8. Android图片滚动,加入自动播放功能,使用自定义属性实现,霸气十足!
  9. Android(安卓)系统拍照及打开系统相册 完美适配 Android(安卓)4

随机推荐

  1. android多线程下载详解
  2. Android(安卓)日期工具类DateUtil
  3. Android(安卓)Studio RadioGroup判断点击
  4. android TV盒子开发遥控器按键的监听
  5. IllegalStateException,PatternSyntaxExce
  6. Android简单的计算控件使用
  7. 彻底解决Android中文乱码
  8. Android(安卓)4.0 UI for Tablet and Han
  9. android ViewPager学习笔记1
  10. android自定义组件