有很多第三方网络库都有三级缓存机制,比如BitmapUtil,ImageLoader等,今天来谈一谈是怎么实现的,首先三级指的是内存,文件,网络三级。OK下面开始写代码了。
对于图片的下载逻辑,我们使用AsyncTask,在这个ImageLoadTask先把总体的框架给写出来,暂时先不管内存缓存,磁盘缓存,与网络请求逻辑怎么实现。
public class ImageLoadTask extends AsyncTask<String, Void, Bitmap> { //使用弱引用包裹ImageView,支持GC,释放之后,不会在设置图片。支持一部分的释放操作:Activity结束,ListView销毁 private final WeakReference<ImageView> imageViewWeakReference; private String url = null; public ImageLoadTask(ImageView imageView) { this.imageViewWeakReference = new WeakReference<ImageView>(imageView); } @Override protected Bitmap doInBackground(String... params) { Bitmap ret = null; if (params != null && params.length > 0) { url = params[0]; int picWidth = 0; int picHeight = 0; int argc = params.length; // 可选参数的执行方式; try { switch (argc){ case 2: //url + width picWidth = Integer.parseInt(params[1]); picHeight = picWidth; break; case 3: // url + width + height picWidth = Integer.parseInt(params[1]); picHeight = Integer.parseInt(params[2]); break; } } catch (NumberFormatException e) { e.printStackTrace(); picWidth = 0; picHeight = 0; } //0、先在内存中找 ImageCache imageCache = ImageCache.getOurInstance(); Bitmap bitmap = imageCache.get(url); if (bitmap == null) { //1.文件缓存,是否存在 FileCache instance = FileCache.getInstance(); byte[] bytes = instance.load(url); if (bytes == null) { //文件不存在,需要从网上下载 bytes = HttpUtil.doGet(url); //存文件缓存 FileCache.getInstance().save(url, bytes); } //文件存在,直接解码 //ret = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); //TODO 检查图片是否过大,过大就缩小,节约内存 ret = loadScaledBitmap(bytes, picHeight, picWidth); imageCache.put(url, ret); bytes = null; } else { ret = bitmap; } } return ret; } /** * 针对图片数据进行二次采样 * * @param data 实际数据 * @param toHeigth * @param toWidth * @return */ private static Bitmap loadScaledBitmap(byte[] data, int toHeigth, int toWidth) { Bitmap ret = null; //1、获取尺寸 //OPtions用来进行解码的配置,对象内部的变量会传递给底层解码, BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;//告诉解码器,只获取尺寸信息,不解析成Bitmap,不占用内存 BitmapFactory.decodeByteArray(data, 0, data.length, options); int outHeight = options.outHeight; int outWidth = options.outWidth; //2缩小图片(使用Options进行缩小的采样) options.inJustDecodeBounds = false;//设置成false,代表正式解码图片,会返回Bitmap options.inSampleSize = calculateInSampleSize(options, toHeigth, toWidth);//图片采样比率,采样比率要求大于等于1,功能是:当设置成2的时候,代表宽度变为二分之一,高度变为二分之一,所以总共变为原始的图片的四分之一 ret = BitmapFactory.decodeByteArray(data, 0, data.length, options); return ret; } @Override protected void onPostExecute(Bitmap bitmap) { if (bitmap != null) { ImageView imageView = imageViewWeakReference.get(); //看看内部对象有没有被释放 if (imageView != null) { Object tag = imageView.getTag(); if (tag != null) { if (tag instanceof String) { String str = (String) tag; if (url.equals(str)) { //TODO 检测是否需要tag处理错位 imageView.setImageBitmap(bitmap); } } } } } } public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (reqWidth > 0 && reqHeight > 0) { if (height >reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) { inSampleSize *= 2; } } } return inSampleSize; } }
这个Task中有图片的压缩逻辑,这个不是三级缓存的重点部分,只是为了性能优化使用。我们首先在内存缓存(ImageCache)中找,找不到去文件缓存(FileCache)中找,如果找到把它加入到内存缓存(ImageCache),如果找不到,就去网络请求,并且将请求之后的结果也放在文件缓存中(FileCache)。
现在看内存缓存怎么写,内存缓存的查找策略是:先从强引用缓存中查找,如果没有再从软引用缓存中查找, 如果在软引用缓存中找到了,就把它移入强引用缓存;如果强引用缓存满了,就会根据Lru算法把某些图片移入软引用缓存,如果软引用缓存也满了, 最早的软引用就会被删除。对Java四大引用还不熟悉的朋友,参考Android性能优化第(一)篇—基本概念。这里需要用到LRU算法,存储URL对应于Bitmap的映射。Lru:Least Recently Used 近期最少使用算法,是一种页面置换算法,其思想是在缓存的页面数目固定的情况下,那些最近使用次数最少的页面将被移出,对于我们的内存缓存来说,强引用缓存大小固定为4M,如果当缓存的图片大于4M的时候, 有些图片就会被从强引用缓存中删除,哪些图片会被删除呢,就是那些近期使用次数最少的图片。
public class ImageCache { private static ImageCache ourInstance = null; /** * 采用LRU算法,存储URL对应于Bitmap的映射,这个对象允许设置存储对象占用的最大的内存空间,或者是条目的最大个数 */ private final LruCache<String, Bitmap> cache;//强引用缓存 private final HashMap<String, SoftReference<Bitmap>> hashCahce;//软引用缓存 private ImageCache() { //最多存30张图片 cache = new LruCache<String, Bitmap>(30); hashCahce = new LinkedHashMap<String, SoftReference<Bitmap>>(); } public static ImageCache getOurInstance() { if (ourInstance == null) { ourInstance = new ImageCache(); } return ourInstance; } public Bitmap get(String url) { Bitmap ret = null; if (url != null) { ret = cache.get(url); if (ret == null) { //如果LRUcache没有,那就看看hashCache SoftReference<Bitmap> reference = hashCahce.get(url); if (reference != null) { ret = reference.get();//获取引用的对象 if (ret == null) { hashCahce.remove(url); } else { //如果hashCache中有,LRU没有 cache.put(url, ret); } } } } return ret; } public void put(String url, Bitmap bitmap) { if (url != null && bitmap != null) { cache.put(url, bitmap); hashCahce.put(url, new SoftReference<Bitmap>(bitmap)); } } }
在看文件缓存怎么去写,关于文件缓存一个注意点就是如何保证每张图片的文件名称不重复,这里使用了MD5, 采用MD5算法,给一个URL,只要URL不一样,算出来一个数,永远不会重复。
public class FileCache { private static FileCache ourInstance; private Context context; private FileCache(Context context) { this.context = context; } /** * 利用Context来初始化FileCache单例对象 * * @param context * @return */ public static FileCache createInstance(Context context) { if (ourInstance == null) { ourInstance = new FileCache(context); } return ourInstance; } /** * 获取单例对象,这个方法调用之前,必须经过createInstace创建实例 * * @return */ public static FileCache getInstance() { if (ourInstance == null) { throw new IllegalStateException("please invoke createIstance(COntext) before this method"); } return ourInstance; } /** * 采用MD5算法----消息摘要,给一个东西,算出来一个数,永远不会重复 * 将网址映射成唯一的文件名 * * @param url * @return */ private static String mapFile(String url) { String ret = null; try { //消息摘要工具 MessageDigest digest = MessageDigest.getInstance("MD5"); //计算出消息摘要 byte[] data = digest.digest(url.getBytes()); //TODO 这个是错误的 //ret=new String(data); //TODO 应该Hex编码,将字节数组的每一个字节利用16进制的形式转化成字符串 ret = EncryptUtil.toHex(data); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ret; } /** * 获取缓存文件夹,支持外部存储和内部存储 * * @return */ private File getCacheFolder() { File ret = null; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { ret = context.getExternalCacheDir(); } else { ret = context.getCacheDir(); } return ret; } /** * 加载缓存数据 * * @param url * @return */ public byte[] load(String url) { byte[] ret = null; if (url != null) { File folder = getCacheFolder(); String fileName = mapFile(url); //文件名称由MD5生成 File f = new File(folder, fileName); if (f.exists() && f.canRead()) { FileInputStream fin = null; try { fin = new FileInputStream(f); ret = StreamUtil.readStream(fin); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { StreamUtil.close(fin); } } } return ret; } public void save(String url, byte[] data) { if (url != null && data != null && data.length > 0) { File folder = getCacheFolder(); String fileName = mapFile(url); File f = new File(folder, fileName); FileOutputStream fos = null; try { fos = new FileOutputStream(f); fos.write(data); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { StreamUtil.close(fos); } } } }
public final class EncryptUtil { public static String toHex(byte data[]) { StringBuilder ret = new StringBuilder(); if (data != null) { int h, l; char ch, cl; for (byte b : data) { h = (b >> 4) & 0x0f;//高四位 l = b & 0x0f;//低四位 if (h > 9) { ch = (char) ('A' + (h - 10)); } else { ch = (char) ('0' + h); } if (l > 9) { cl = (char) ('A' + (l - 10)); } else { cl = (char) ('0' + l); } ret.append(ch).append(cl); } } return ret.toString(); } }
三级缓存写好了,最后在Adapter中使用如下。
if (url != null) { ImageLoadTask task = new ImageLoadTask(imageView); task.execute(url, "60", "60");//60是宽高 }
作者:Looper景