安卓开发 item bitmap 大小 bitmap android_加载

Learn && Live

虚度年华浮萍于世,勤学善思至死不渝

前言

Hey,欢迎阅读Connor学Android系列,这个系列记录了我的Android原理知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:,话不多说我们马上开始!

1.Bitmap 的高效加载

1.1 Bitmap 的加载

(1)Bitmap 在 Android 中指的是一张图片,可以是 png 格式也可以是 jpg 等其他常见的图片格式

(2)Bitmap 的加载可以通过 BitmapFactory 完成,这个类提供了四类方法

  • decodeFile:从文件系统中加载出一个 Bitmap 对象
  • decodeResource:从资源中加载出一个 Bitmap 对象
  • decodeStream:从输入流中加载出一个 Bitmap 对象
  • decodeByteArray:从字节数组中加载出一个 Bitmap 对象
  • 其中 decodeFile 和 decodeResource 会间接调用 decodeStream 方法

(3)BitmapFactory 中的这四类方法最终是在 Android 的底层实现的,对应 BitmapFactory 类的几个 native 方法

1.2 Bitmap 的高效加载

(1)由于 Bitmap 的特殊性以及 Android 对应用的内存限制(16MB),导致加载 Bitmap 时很容易出现 OOM,因此需要更高效地加载

(2)Bitmap 高效加载的核心思想就是采用 BitmapFactory.Options 按一定的采样率来加载缩小后的图片,将缩小后的图片显示在页面上,这样就可以通过降低图片的内存占用,从而一定程度上避免了 OOM

(3)BitmapFactory 提供的四类方法都支持 BitmapFactory.Options 参数,通过它们就可以很方便地对一个图片进行采样缩放

(4)BitmapFactory.Options 缩放图片主要用到了它的 inSampleSize 参数,即采样率

  • inSampleSize <= 1,采样后的图片大小为图片的原始大小
  • inSampleSize > 1,缩放图片,缩放比例为 1 / (inSampleSize ^ 2)
  • inSampleSize 推荐为 2 的整数次方,否则会向下取整并选择一个最接近的 2 的整数次方

(5)通过采样率即可有效地加载图片,那应如何获取采样率呢?过程如下

  • 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 true 并加载图片,这里说明一下这个参数
  • 此参数为 true,BitmapFactory 只会解析图片的原始宽、高及 MIME 类型信息,并不会真正地加载图片,是轻量级的操作
  • 从 BitmapFactory.Options 中取出图片的原始宽高信息,对应 outWidth 和 outHeight 参数
  • 注意此时获取的图片宽高信息和图片的位置(不同的 drawable 目录下)以及硬件设备(屏幕密度不同)有关
  • 根据采样率的规则并结合目标 View 的所需大小计算出采样率 inSampleSize
  • 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 false 并重新加载图片,此时就是真正加载缩小后的图片了

2.Android 中的缓存策略

2.1 基本介绍

缓存

Android 的缓存可用于避免过多的流量消耗,一般设计为三级的缓存机制

(1)当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不需要再从网上获取了

(2)很多时候还会在内存中再缓存一份

(3)这样当应用要请求一张图片时,会首先从内存中获取,如果没有再从存储设备中获取,如果还没有就从网络上下载

缓存策略

(1)缓存策略主要包含缓存的添加、获取和删除,当缓存容量满了,则需要删除一些旧缓存来添加新缓存

(2)如何定义缓存的新旧就是一种缓存的策略,不同的策略对应不同的缓存算法

(3)常用的缓存算法是 LRU,最近最少使用算法,即优先淘汰最近最少使用的缓存对象,采用该算法的缓存主要有两种

  • LruCache:用于实现内存缓存
  • DiskLruCache:用于实现存储设备缓存
2.2 LruCache

(1)LruCache 是一个泛型类,内部采用一个 LinkedHashMap 以强引用的方式存储外界的缓存对象

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

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

(2)创建LruCache时只需要提供缓存的总容量大小并重写 sizeOf 方法即可

  • 容量一般根据当前线程的可用内存来设置,单位是 KB:Runtime.getRuntime().maxMemory()
  • sizeOf 方法用于计算缓存对象,即 Bitmap 对象的大小
  • 此外,一些特殊情况下,还需要重写 entryRemoved 方法,当移除旧缓存时会调用该方法,可以完成一些资源回收工作

(3)通过 get、set 方法完成缓存的获取和添加操作

(4)还支持删除操作,可通过 remove 方法删除一个指定的缓存对象

(5)当缓存满时,会移除较早使用的缓存对象,然后再添加新的缓存对象

2.3 DiskLruCache

DiskLruCache 用于实现存储设备缓存,即磁盘缓存,通过将缓存对象写入到文件系统中实现缓存效果

创建

DiskLruCache 不能通过构造方法来创建,需要通过 open 方法来完成,open 方法共有四个参数

(1)第一个参数表示磁盘缓存在文件系统中的存储路径,可以选择 SD 卡上的缓存目录,也可以选择 SD 卡上的其他指定目录

  • 如果应用卸载后就删除缓存文件,那么就选择 SD 卡上的缓存目录
  • 如果希望保留缓存数据就应该选择 SD 卡上的其他指定目录

(2)第二个参数表示应用的版本号,一般设为1即可。当版本号发生改变时会清空之前所有的缓存文件,但在实际开发中并非那么有效

(3)第三个参数表示单个节点所对应的数据的个数,一般设为1即可

(4)第四个参数表示缓存的总大小,单位是 B,超出这个值时会清楚一些缓存来保证总大小不大于这个设定值

private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;

File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if(!diskCacheDir.exists()) {
    diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);

添加

缓存添加的操作是通过 Editor 完成的,Editor 表示一个缓存对象的编辑对象,添加步骤如下

(1)首先需要获取图片 url 对应的 key

  • 这里使用 key 而非直接使用 url 是因为 url 中很可能有特殊字符,会影响它 Android 中的直接使用,一般采用 url 的 md5 值作为 key
private String hashKeyFormUrl(String url) {
    String cacheKey;
    try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(url.hashCode());
    }
    return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < bytes.length; ++i) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if(hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

(2)根据 key 通过 edit 方法获取 Editor 对象,获取到后即可根据它来得到一个文件输出流

  • 如果当前不存在其他 Editor 对象,则 edit 方法会返回一个新的 Editor 对象
  • 如果这个缓存正在被编辑,则 edit 方法会返回 null,即 DiskLruCache 不允许同时编辑一个缓存对象
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
    OutputStream outputstream = editor.newOutputStream(DISK_CACHE_INDEX);
}

(3)当我们从网络下载图片时,就可以通过拿到的文件输出流写入到文件系统上了

public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpUrlConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    
    try {
        final URL url = new URL(urlString);
        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) {
        ...
    } finally {
        if (urlConnection != null) {
            urlConnection.disconnect();
        }
        MyUtils.close(out);
        MyUtils.close(in);
    }
    return false;
}

(4)此时还没有真正地将图片写入文件系统,还必须调用 Editor 的 commit 方法来提交写入操作,如果图片下载过程中发生了异常,还可以通过 Editor 的 abort 方法来回退整个操作

OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
    editor.commit();
} else {
    editor.abort();
}
mDiskLruCache.flush();

查找

和缓存的添加过程类似

(1)将 url 转换为 key

(2)根绝 key 调用 DiskLruCache 的 get 方法获取一个 Snapshot 对象

(3)根据 Snapshot 对象即可得到缓存的文件输入流,进而得到 Bitmap 对象

(4)一般不建议直接加载原始图片,而是加载缩放图片

  • 但之前介绍的缩放方法会对 FileInputStream 的缩放存在问题,因为 FileInputStream 是有序的文件流,而两次 decodeStream 调用影响了文件流的位置属性,会导致第二次 decodeStream 时得到的是 null
  • 可以通过文件流来获得其对应的文件描述符,然后再通过 BitmapFactory.decodeFileDescriptor 方法来加载一张缩放后的图片
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
    FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
    FileDescriptor fileDescriptor = fileInputStream.getFD();
    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
    if (bitmap != null) {
        addBitmapToMemoryCache(key, bitmap);
    }
}

删除

可通过 remove、delete 方法完成对磁盘缓存的删除操作