admin 发表于 2017-2-13 15:26:24

Android 加载大量图片的三级缓存处理

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;
            url = (String) params;
            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
页: [1]
查看完整版本: Android 加载大量图片的三级缓存处理