在做新闻客户端的时候,有大量网络图片装载在ImageView显示,发现加载图片的时候
经常会出现OOM异常,这时候我上网查了不少资料,发现,其实图片加载的时候没必要
每次都从网络拉去,这时候就要用到缓存机制。经过查资料发现,图片缓存基本分为三级缓存:
- 网络缓存
- 内存缓存
- 本地缓存
经过网上查询大量资料得出一些心得,下面一一详细说明。
网络缓存:
其实我觉得网络拉区图片也不算缓存,但是既然江湖规矩就是这样,我也把它称为网络
缓存算了,网络缓存实质是从网络拉去图片显示在ImageView中,下面代码中我写了一个
NetCacheUtil类作为网络缓存,我在其中用了AsyncTask 异步任务来实现下载功能。代码比较简单:
public class NetCacheUtil {
private LocalCacheUtil mLocalCacheUtil;
public NetCacheUtil(LocalCacheUtil mLocalCacheUtil) {
this.mLocalCacheUtil = mLocalCacheUtil;
}
/**
* 从网络获取图片并显示在ImageView中
*
* @param ivPic
* 要显示图片的ImageView
* @param url
* 图片URL
*/
public void getBitmapFromNet(ImageView ivPic, String url) {
new BitmapTask().execute(ivPic, url); // 启动
}
class BitmapTask extends AsyncTask<Object, Void, Bitmap> {
private ImageView ivPic;
private String url;
@Override
protected Bitmap doInBackground(Object... params) {
ivPic = (ImageView) params[0];
url = (String) params[1];
ivPic.setTag(url);
return downloadBitmap(url);
}
@Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
if (ivPic.getTag().equals(url))
//把Bitmap添加到本地缓存
mLocalCacheUtil.setBitmapToLocal(url, result);
ivPic.setImageBitmap(result);
}
}
}
本地缓存
关于本地缓存我同样写了一个LocalCache来维护,里面使用了SD卡或者ROM内存保存了Bitmap位图,当加载ImageView 的时候优先使用本地缓存:
/**
* 本地缓存
*
* @author Administrator
*/
public class LocalCacheUtil {
// 路径
public static final String CACHE_PATH = Environment
.getExternalStorageDirectory() + "/packageName/";
// 当sd卡不可用的时候使用rom内存
public static final File cacheFile = BaseApplication.getContext()
.getCacheDir();
/**
* 从本地读取数据
* @param url图片的保存地址
*/
public Bitmap getBitmapFromLocal(String url) {
File file;
String fileName = md5(url);
// 当sdcard卡已经挂载的时候
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
file = new File(CACHE_PATH, fileName);
} else {
file = cacheFile;
}
if (file.exists()) {
try {
return BitmapFactory.decodeStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 将文件名用md5 加密
* @param str
* @return
*/
public String md5(String str) {
try {
MessageDigest instance = MessageDigest.getInstance("MD5");
byte[] digest = instance.digest(str.getBytes());
StringBuffer sb = new StringBuffer();
for (byte b : digest) {
int i = b & 0xFF;
String hex = Integer.toHexString(i);
if (hex.length() < 2) {
hex = "0" + hex;
}
sb.append(hex);
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 把图片保存到本地
*
* @param url 图片路径
* @param bitmap 位图对象
*/
public void setBitmapToLocal(String url, Bitmap bitmap) {
File file;
String fileName = md5(url);
// 当sdcard卡已经挂载的时候
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
file = new File(CACHE_PATH, fileName);
} else {
file = cacheFile;
}
try {
// 如果文件不存在就创建
if (!file.getParentFile().exists()) {
file.getParentFile().mkdir();
}
// 把Bitmap对象以jpg格式保存
bitmap.compress(CompressFormat.JPEG, 100,
new FileOutputStream(file));
} catch (Exception e) {
e.printStackTrace();
}
}
}
内存缓存
当然了,内存缓存是最重要的一块,同是也是最难处理的一块,内存缓存一个不小心就会导致OOM异常,开始的时候我是打算用一个HashMap来保存Bitmap对象,其中以图片URL作为key,Bitmap对象作为值。代码如下:
/**
* 内存缓存
*
* @author Administrator
*/
public class MemoryCache {
// 维护Bitmap的集合
private HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap>();
/**
* 从内存缓存中获取Bitmap
*
* @param url
* 图片URL
* @return
*/
public Bitmap getBitmapFromMemoryCache(String url) {
Bitmap bitmap = mMemoryCache.get(url);
return bitmap == null ? null : bitmap;
}
/**
* 把Bitmap写进内存
* @param bitmap
* @param url
*/
public void setBitmapToMemoryCache(Bitmap bitmap, String url) {
mMemoryCache.put(url, bitmap);
}
}
我在外部加载图片的代码是这样的:
public class MyBitmapUtils {
NetCacheUtil mNetCacheUtil;
LocalCacheUtil mLocalCacheUtil;
MemoryCacheUtil mMemoryCacheUtil;
public MyBitmapUtils() {
mMemoryCacheUtil = new MemoryCacheUtil();
mLocalCacheUtil = new LocalCacheUtil();
mNetCacheUtil = new NetCacheUtil(mLocalCacheUtil);
}
/**
* 把url对应的图片显示在ImageView中
* @param ivPic
* @param url
*/
public void display(ImageView ivPic, String url) {
ivPic.setImageResource(R.drawable.default);// 设置默认加载图片
Bitmap bitmap = null;
// 从内存读取图片
bitmap = mMemoryCacheUtil.getBitmapFromMemoryCache(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
System.out.println("从内存读取图片啦...");
return;
}
// 从本地读取图片
bitmap = mLocalCacheUtil.getBitmapFromLocal(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
System.out.println("从本地读取图片啦...");
//从本地读取图片成功后把图片添加到内存缓存中
mMemoryCacheUtil.setBitmapToMemoryCache(bitmap, url);
return;
}
// 从网络读图片
mNetCacheUtil.getBitmapFromNet(ivPic, url);
}
}
上面的类我做了一些处理,当我要加载一张图片的时候,我先从内存中获取,如果返回
值为空,我再从本地文件中读取,如果返回值也是null,这时候我才从网络拉取数据,
这样的3级缓存机制可以减少用户流量的使用。
当我写好上面3个工具类之后,满心欢喜去跑在模拟器上面,我滑啊滑啊,不同的页面来
回切,然后,程序还是崩了。
后来我仔细阅读了我的代码,发现我在内存缓存中维护的HashMap的size 一直在变大,
这说明了什么? 说明系统根本不会回收我HashMap的对象。后来又继续网上查资料,发现在Android 系统中默认给每个应用分配16M的内存,而Java语言中引用分为四种,下面我简单说一下特点:
- 强引用: 一般引用,这是使用最普遍的引用。如果一个对象具有强引用,垃圾回
收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError
错误,也不会靠随意回收具有强引用的对象来解决内存不足问题。 - 软引用(SoftReference):如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
- 弱引用(WeakReference):弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
- 虚引用(PhantomReference):如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
这样一看,好像只有软引用适合我这种情况了,然后我就改造程序,把存进HashMap中的Bitamp对象设置为软引用。改造后的MemoryCacheUtil代码如下:
/**
* 内存缓存
*
* @author Administrator
*/
public class MemoryCacheUtil {
// 维护Bitmap的集合
private HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<String, SoftReference<Bitmap>>();
/**
* 从内存缓存中获取Bitmap
*
* @param url
* 图片URL
* @return
*/
public Bitmap getBitmapFromMemoryCache(String url) {
SoftReference<Bitmap> softReference = mMemoryCache.get(url);
if (softReference != null) {
Bitmap bitmap = softReference.get();
return bitmap;
}
return null;
}
/**
* 把Bitmap写进内存
*
* @param bitmap
* @param url
*/
public void setBitmapToMemoryCache(Bitmap bitmap, String url) {
SoftReference<Bitmap> b = new SoftReference<Bitmap>(bitmap);
mMemoryCache.put(url, b);
}
}
嗯,代码改造完毕后,我在模拟器上面滑来滑去,然后看后台输出,显示从内存读取的
情况寥寥无几,大部分都是从本地读取的。从结果看来,我猜应该是Bitmap对象存进去
的时候还没来得及复用就被系统回收了,这尼玛坑啊,搞了半天白干了。
后来又上网找资料,发现了一个牛逼的东西。在Android 3.0以上版本提供了一个特殊的类叫做LruCache,是Android Support V4 包里面提供的。然后代码再次改造。结果如下:
/**
* 内存缓存
*
* @author Administrator
*/
public class MemoryCacheUtil {
// 维护Bitmap的集合
private LruCache<String, Bitmap> mLruCache;
public MemoryCacheUtil() {
long maxMemory = Runtime.getRuntime().maxMemory() / 8;// 模拟器默认是16M
mLruCache = new LruCache<String, Bitmap>((int) maxMemory) {
@Override
protected int sizeOf(String key, Bitmap value) {
int byteCount = value.getRowBytes() * value.getHeight();
return byteCount;
}
};
}
/**
* 从内存缓存中获取Bitmap
*
* @param url
* 图片URL
* @return
*/
public Bitmap getBitmapFromMemoryCache(String url) {
Bitmap bitmap = mLruCache.get(url);
return bitmap == null ? null : bitmap;
}
/**
* 把Bitmap写进内存
*
* @param bitmap
* @param url
*/
public void setBitmapToMemoryCache(Bitmap bitmap, String url) {
mLruCache.put(url, bitmap);
}
}
大家看代码也知道了,其实LruCache的用法和HashMap的用法差不多,只是需要在创建对象的时候传进去一个最大占用内存数,并且复写一个sizeOf方法,该方法返回一个对象所占用的字节数。
然后我比较好奇LruCache内部是怎么实现的,我就去翻一番它的源码,发现其实他的代
码就那么几百行。也是用一个LinkedHashMap实现的。其中它内部自己维护,当长度过大的时候就移除一个元素,核心代码如下:
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
好,终极版三级缓存处理新鲜出炉,然后试了下效果,在不断切换滑动的时候,读取的大部分都是内存缓存,程序也能坚持很久。但是最后也是会蹦。这个时候真的心累了,没辙了啊。
继续查查查,查到图片压缩这个功能,在从网络读取数据的时候把输入流构造成Bitmap的时候可以传进去一个参数:BitmapFactor.Options
该参数可以设置压缩比例,同时还可以设置图片格式等等参数,通过这些属性设置来让内存负荷减少是一种有效的方法,当然了,这些参数要设置得合理,不然显示不出图片效果。
- option.inSimpleSize = 2 ; //压缩为二分之一,该值要根据图片要展示的大小来确定
- option.inPreferredConfig = Bitmap.Config.RGB_565;设置图片格式
这个缓存机制有点坑爹啊,其实做了这么多处理,经过测试,还是有可能程序会崩掉的。