版权声明:本文为LooperJing原创文章,转载请注明出处!
有很多第三方网络库都有三级缓存机制,比如BitmapUtil,ImageLoader等,今天来谈一谈是怎么实现的,首先三级指的是内存,文件,网络三级,严格来说网络不能算是缓存,但是更完整,常常把网络加在里面。OK下面开始写代码了。
对于图片的下载逻辑,我们使用AsyncTask,在这个ImageLoadTask先把总体的框架给写出来,暂时先不管内存缓存,磁盘缓存,与网络请求逻辑怎么实现。
public class ImageLoadTask extends AsyncTask {
//使用弱引用包裹ImageView,支持GC,释放之后,不会在设置图片。支持一部分的释放操作:Activity结束,ListView销毁
private final WeakReference imageViewWeakReference;
private String url = null;
public ImageLoadTask(ImageView imageView) {
this.imageViewWeakReference = new WeakReference(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 cache;//强引用缓存
private final HashMap> hashCahce;//软引用缓存
private ImageCache() {
//最多存30张图片
cache = new LruCache(30);
hashCahce = new LinkedHashMap>();
}
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 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));
}
}
}
在看文件缓存怎么去写,关于文件缓存一个注意点就是如何保证每张图片的文件名称不重复,这里使用了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是宽高
}
Please accept mybest wishes for your happiness and success !