Android 加载大量图片的三级缓存处理。
在做新闻客户端的时候,有大量网络图片装载在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;设置图片格式
这个缓存机制有点坑爹啊,其实做了这么多处理,经过测试,还是有可能程序会崩掉的。 http://blog.csdn.net/u012943767/article/details/48186733
|