[Android开发艺术探索阅读笔记]第12章 Bitmap 的加载和缓存

本章的主题是Bitmap的加载和Cache,主要包含三个方面的内容。首先讲述如何有效地加载一个Bitmap,接着介绍Android中常用的缓存策略,最后本章会介绍如何优化列表的卡顿现象。

Bitmap 的高效加载

BitmapFactory 类提供了四类方法:

  • decodeFile()
  • decodeResource()
  • decodeStream()
  • decodeByteArray()

decodeFile()decodeResource()decodeResource() 最终都是调用了 decodeStream()decodeStream() 内部调用的是底层的 native 方法。

高效加载 Bitmap 的核心思想就是采用 BitmapFactory.Options 加载所需尺寸的图片。

很多时候,ImageView 的尺寸并没有图片的原始尺寸那么大,这时显然没有必要按原始尺寸加载,ImageView 用多少我们就给他多少。

通过 BitmapFactory.Options 将图片缩小后再加载,这样降低内存占用,从而在一定程度上避免 OOM。

BitmapFactory.Options

主要用到了 BitmapFactory.Options 中的 inSampleSize 参数。即采样率。

采样后的高 = 采样前的高 / insampleSize
采样后的宽 = 采样前的宽 / insampleSize
采样后所占内存大小 = 采样前所占内存大小 / insampleSize^2

  • 如果 inSampleSize < 1,那么等同于 1
  • inSampleSize 必须是 2 的幂次方,设置其他值会向下取与它最接近的幂次方的值。比如设置 3,那么系统会取 2。

使用流程:

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

inJustDecodeBounds = true 时,BitmapFactory 只会解析图片的原始宽/高信息,并不会真正的去加载图片。

  public static Bitmap decodeSampleBitmap(Resources resources, int resId, int reqWidth,
      int reqHeight) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(resources, resId, options);
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(resources, resId, options);
  }

  private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,
      int reqHeight) {
    // 原始的宽和高
    final int width = options.outWidth;
    final int height = options.outHeight;
    Log.d(TAG, "origin, w= " + width + " h=" + height);

    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
      // 为了将 inSampleSize 设置为 2 的幂次方,先将原始高度 /2
      final int halfHeight = height / 2;
      final int halfWidth = width / 2;

      // 将 inSampleSize 不断 * 2 直到宽和高有一个满足需要时
      while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
        inSampleSize *= 2;
      }
    }
    Log.d(TAG, "sampleSize:" + inSampleSize);
    return inSampleSize;
  }
复制代码

Android 中的缓存策略

常用的缓存算法是 LRU(Least Recently Used),即近期最少使用算法,核心思想是:当缓存满了时,优先淘汰近期最少使用的缓存对象。
LruCache 实现内存缓存。
DiskLruCache 用于设备存储缓存。

LruCache

LruCache 是 Android 3.1 所提供的一个缓存类,通过 support-v4 兼容包可以兼容到早期的 Android 版本。

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;
    private int maxSize;

    private int putCount;
    private int createCount;
    private int evictionCount;
    private int hitCount;
    private int missCount;
 
 // ...   
}
复制代码

它内部通过一个 LinkedHashMap 以强引用的方式存储外界的缓存对象。

  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被 gc 回收
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被 gc 回收
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override protected int sizeOf(String key, Bitmap value) {
        // 计算 bitmap 的大小 单位:m
        // getRowBytes(): 用于计算位图每一行所占用的内存字节数
        // 经实测发现:位图大小getByteCount() = getRowBytes() * getHeight(),也就是说位图所占用的内存空间数等于位图的每一行所占用的空间数乘以位图的行数。
        // https://yq.aliyun.com/articles/32572
        return value.getRowBytes() * value.getHeight() * 1024;
    }

    @Override
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
        super.entryRemoved(evicted, key, oldValue, newValue);
        // 做一些资源回收工作
    }
};

// 取
Bitmap key = lruCache.get("key");
// 存
lruCache.put("key",bitmap);
// 删
lruCache.remove("key")

复制代码

DiskLruCache

DiskLruCache 得到了 Android 官方文档的推荐,但它不属于 Android SDK 的一部分,它的源码可以从如下网址得到: https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java。但是在 Android 会发生编译错误。

JakeWharton 提供了一份没有编译错误的: https://github.com/JakeWharton/DiskLruCache

compile 'com.jakewharton:disklrucache:2.0.2'
复制代码

创建

通过 open 方法创建自身:

/**
   * Opens the cache in {@code directory}, creating a cache if none exists
   * there.
   *
   * @param directory 磁盘缓存文件所在的路径
   * @param appVersion 版本号
   * @param valueCount 单个节点所对应的数据的个数,一般设为1即可
   * @param maxSize 缓存的最大值
   * @throws IOException if reading or writing the cache directory fails
   */
  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException
复制代码
final long DISK_CACHE_SIZE = 1024 * 10224 * 50; // 大小为50m
    try {
      PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
      DiskLruCache open =
          DiskLruCache.open(getCacheDir(), packageInfo.versionCode, 1, DISK_CACHE_SIZE);
    } catch (IOException | PackageManager.NameNotFoundException e) {
      e.printStackTrace();
    }
复制代码

缓存添加

通过 Editor 完成。Editor 表示一个缓存对象的编辑对象。

以图片缓存为例,用图片 url 的 md5 值作为 key。

// 将url转成md5
private String hashKeyFromUrl(String url) {
    String cacheKey;
    try {
      MessageDigest md5 = MessageDigest.getInstance("MD5");
      md5.update(url.getBytes());
      cacheKey = bytesToHexString(md5.digest());
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      cacheKey = String.valueOf(url.hashCode());
    }
    return cacheKey;
  }

  // 将字节码转成16进制
  private String bytesToHexString(byte[] digest) {
    StringBuilder builder = new StringBuilder();
    for (byte aDigest : digest) {
      String hex = Integer.toHexString(0xFF & aDigest);
      if (hex.length() == 1) {
        builder.append('0');
      }
      builder.append(hex);
    }
    return builder.toString();
  }
复制代码
// 将图片下载写入 DiskLurCache 的输出流里
  private boolean downloadUrlToStream(String urlS, OutputStream outputStream) throws IOException {
    final int IO_BUFFER_SIZE = 1024;
    HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;

    try {
      final URL url = new URL(urlS);
      urlConnection = (HttpURLConnection) url.openConnection();
      in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
      out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);

      int b;
      while ((b = in.read()) != -1) {
        out.write(b);
      }
      return true;
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (urlConnection != null) urlConnection.disconnect();
      if (out != null) {
        out.close();
      }
      if (in != null) {
        in.close();
      }
    }
    return false;
  }
复制代码

使用:

DiskLruCache diskLruCache = openDiskLruCache();
    String key = hashKeyFromUrl("url");
    try {
      if (diskLruCache != null) {
        DiskLruCache.Editor edit = diskLruCache.edit(key);
        // 获得文件输出流
        if (edit != null) {
          OutputStream outputStream = edit.newOutputStream(DISK_CACHE_INDEX);
          if (downloadUrlToStream("imageUrl",outputStream)){
            // 提交写入操作
            edit.commit();
          }else {
            // 发生错误就回滚整个操作
            edit.abort();
          }
        }

      }
    } catch (IOException e) {
      e.printStackTrace();
    }
复制代码

缓存查找

通过存入时的 key 去找到对应的 Snapshot 对象, 通过它即可得到缓存的文件输入流。

避免加载过程中导致的 OOM,应该通过 BitmapFactory.Options 对象来加载一张缩放后的图片,但是这种方法对 FileInputStream 的缩放存在问题。

FileInputStream 是一种有序的文件流,两次 decodeStream 调用影响了文件流的位置属性,导致第 2 次 deocdeSream 时得到的时 null。

解决:
通过文件流来得到它所对应的文件描述符,然后再通过 BitmapFactory.decodeFileDescriptor() 来加载一张缩放后的图片。

 try {
      Bitmap bitmap;
      String key = hashKeyFromUrl("url");
      DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
      if (snapshot!=null){
        FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
        // 获取文件描述符
        FileDescriptor fd = inputStream.getFD();
        
        // 再通过 `BitmapFactory.decodeFileDescriptor()` 来加载一张缩放后的图片
        bitmap = BitmapUtils.decodeSampledBitmapFromFileDescriptor(fd,reqW,reqH);
        if (bitmap!=null) {
          addToDiskCache(diskLruCache,"url",bitmap);
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
复制代码

列表的卡顿

  • 在 ListView 的 getView() 中不应做耗时任务。
  • 控制异步任务执行的频率
  • 开启硬件加速 android:hardwareAccelerated="true"
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值