Bitmap 的加载和 Cache

Android 中如何高效地加载 Bitmap 是一个很重要也很容易被我们忽视的问题。

Bitmap 的高效加载

  1. BitmapFactory 类提供了:decodeFiledecodeResourcedecodeStreamdecodeByteArray 以及 decodeFileDescriptor 等几类方法来加载一个 Bitmap 对象。
  2. 高效加载 Bitmap 的核心思想就是设置 BitmapFactory.OptionsinSampleSize 采样率属性来加载所需尺寸的图片。

    inSampleSize = 1 时加载原图,inSampleSize = 2 时加载的像素为原图的 1/4,以此类推。官方推荐设置 inSampleSize 的值为 2 的指数。

  3. 获取采样率的流程:

    1. BitmapFactory.OptionsinJustDecodeBounds 参数设为 true 并加载图片。
    2. BitmapFactory.Options 中取出图片的原始宽高信息,它们对应于 outWidthoutHeight
    3. 根据采样率的规则并结合目标 View 的所需大小计算出采样率 inSampleSize
    4. BitmapFactory.OptionsinJustDecodeBounds 参数设为 false 然后重新加载图片。


      示例代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
       public Bitmap decodeSampledBitmapFromResource(Resources res,
      int resId, int reqWidth, int reqHeight) {
      // 设置 inJustDecodeBounds = true 时 BitmapFactory 只会解析图片的原始宽高等信息。
      // 并不会真正地去加载图片
      final BitmapFactory.Options options = new BitmapFactory.Options();
      options.inJustDecodeBounds = true;
      BitmapFactory.decodeResource(res, resId, options);

      // 计算采样率
      options.inSampleSize = calculateInSampleSize(options, reqWidth,
      reqHeight);

      // 重新加载图片
      options.inJustDecodeBounds = false;
      return BitmapFactory.decodeResource(res, resId, options);
      }

      public int calculateInSampleSize(BitmapFactory.Options options,
      int reqWidth, int reqHeight) {
      if (reqWidth == 0 || reqHeight == 0) {
      return 1;
      }

      // 图片的原始宽高信息
      final int height = options.outHeight;
      final int width = options.outWidth;

      int inSampleSize = 1;

      if (height > reqHeight || width > reqWidth) {
      final int halfHeight = height / 2;
      final int halfWidth = width / 2;

      while ((halfHeight / inSampleSize) >= reqHeight
      && (halfWidth / inSampleSize) >= reqWidth) {
      inSampleSize *= 2;
      }
      }

      return inSampleSize;
      }


Android 中的缓存策略

  1. 缓存策略并没有统一的标准,一般来说缓存策略主要包含缓存的添加、获取和删除这三类操作。
  2. 常用的缓存算法是 LRU(Least Recently Used),翻译为:近期最少使用算法。它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。
  3. Android 中常见的 LRU 算法缓存有两种:LruCache(内存缓存) 和 DiskLruCache(存储设备缓存),一般情况下会将两者结合使用。
LruCache
  1. LruCache 是一个泛型类,内部采用一个 LinkedHashMap<K, V> 以强引用的方式存储外界的缓存对象,提供了 getput 等操作方法,当存储满时,会移除较早使用的缓存对象,再添加新的缓存对象。此外,LruCache 是线程安全的。

    三种引用的区别:

    • 强引用:直接的对象引用
    • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被 gc 回收
    • 弱引用:当一个对象只有弱引用存在时,此对象会随时被 gc 回收
  2. LruCache 典型示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 获取当前可用的最大内存
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // KB
    // 设置缓存大小
    int cacheSize = maxMemory / 8;

    LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
    // 重写此方法计算缓存对象的大小
    return value.getRowBytes() * value.getHeight() / 1024;
    }
    };
    // 添加缓存
    lruCache.put("liyu", bitmap);
    // 获取缓存
    Bitmap bitmap = lruCache.get("liyu");
DiskLruCache

DiskLruCache 用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存的效果。项目地址:https://github.com/JakeWharton/DiskLruCache

  1. DiskLruCache 的创建

    1
    2
    3
    4
    5
    6
    7
     /**
    * @param directory 缓存路径
    * @param appVersion 一般为 1,当版本号变更时,会清空缓存文件
    * @param valueCount 单个节点所对应的数据个数,一般为 1
    * @param maxSize 缓存总大小,超过的话会清除一些缓存
    */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
  2. DiskLruCache 的缓存添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    String key = hashKeyFormUrl(url); // url 转换下防止特殊字符影响
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX); // DISK_CACHE_INDEX 为 0,因为设置的一个节点只有一个数据
    if (downloadUrlToStream(url, outputStream)) { //下载
    editor.commit(); // 下载成功提交缓存
    } else {
    editor.abort(); // 出现异常则取消缓存
    }
    mDiskLruCache.flush(); // 强制缓冲文件保存到文件系统
    }
  3. DiskLruCache 的缓存查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Bitmap bitmap = null;
    String key = hashKeyFormUrl(url); // url 转换下防止特殊字符影响
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); // 获取缓存快照
    if (snapShot != null) {
    FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX); // DISK_CACHE_INDEX 为 0,因为设置的一个节点只有一个数据
    FileDescriptor fileDescriptor = fileInputStream.getFD(); // 解决 decodeStream 缩放 bitmap 第二次为 null 的问题
    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
    reqWidth, reqHeight); // 缩放图片
    if (bitmap != null) {
    addBitmapToMemoryCache(key, bitmap); // 添加到内存缓存,方便下次快速获取
    }
    }
ImageLoader 的实现

一个优秀的图片加载器应具备:

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取

完整的 ImageLoader 示例可以参考源码

ImageLoader 使用

  1. 非 WiFi 环境下使用时应有提示,避免消耗过多流量。
  2. 列表视图例如 ListView,不要在 getView 中执行耗时操作。
  3. 控制异步任务的执行频率,例如列表滚动的时候不加载图片,等完全停止时才开始加载。
  4. 如果仍有卡顿现象,可以尝试开启硬件加速。