当前位置: 首页 > 工具软件 > DiskLruCache > 使用案例 >

Android 缓存策略LruCache和DiskLruCache

景宏盛
2023-12-01

1 BitmapFactory

对于加载Bitmap,系统提供了 BitmapFactory 类并提供四类方法:decodeFile()decodeResource()decodeStream()decodeByteArray()

分别用于支持文件系统、资源、输入流以及字节数组中加载一个Bitmap对象,其中 decodeFile()decodeResource() 内部间接调用了 decodeStream() 方法。

2 BitmapFactory.Options

通过 BitmapFactory.Options 来缩放图片,主要用 inSampleSize 即采样率。

一般inSampleSize设置的大小以2的次方形式对图片宽高、缩放比例进行影响,对图片宽高为 1/inSampleSize,对缩放比例(像素数、占有内存大小)为 1/(inSampleSize的2次方)

  • 当inSampleSize=1,采样后的图片大小为图片原始宽高,像素数为原图大小,占有内存大小为原图大小

  • 当inSampleSize=2,采样后的图片大小为图片原始宽高的1/2,像素数为原图大小的1/4,占有内存大小为原图大小的1/4

如:

一张图片 1024*1024,假定采用 ARGB8888 格式存储,占有内存为 1024*1024*4,图片大小为 4MB
设置inSampleSize=2,采样后的图片大小为 512*512*4 ,图片大小为 1MB

3 获取采样率缩放图片

  • 设置 BitmapFactory.OptionsinJustDecodeBounds 为true(此时不会加载图片,会对图片进行解析)

  • BitmapFactory.Options 获取图片的原始宽高信息,对应 outWidthoutHeight

  • 结合所需要的图片大小计算采样率 inSampleSize

  • 设置 BitmapFactory.Options 的i nJustDecodeBounds 为false,重新加载图片

public static Bitmap decodeSampleBitmapFromResource(Resource res, int resId, int reqWidth, int reqHeight) {
    final BitmapFactory.Options = new BitmapFactory.Options();
    //开始解析图片
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    //结束解析图片
    options.inJustDecodeBound = false;
    
    return BitmapFactory.decodeResource(res, resId, options);   
}

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    final int width = options.outWidth;
    final int height = options.outHeidht;
    int inSampleSize = 1; //设置默认采样率,原始图片宽高
    
    //计算采样率inSampleSize
    [1]要保证 原始图片宽高>需要的宽高(如果原始图片宽高小于我们需要的宽高,图片会被拉伸导致模糊),否则直接默认采样率inSampleSize
    [2]如果第一个条件成立,通过循环计算inSampleSize,直到 图片宽高>=需要的宽高
    if (width > reqWidth || heidht > reqHeight) {
        final int halfWidth = width / 2;
        final int halfHeigth = height / 2;

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

    return inSampleSize;
}

注意:在实际项目开发中,要缩放的图片原始大小和图片被放置的分辨率目录有关,比如图片放在 mipmap-hdpimipmap-xhdpi 图片大小就不同

4 缓存策略

Lru(Least Recently Used) 即最近最少使用算法,当缓存满时,会优先淘汰那些近期最少使用的缓存对象。
采用Lru算法的缓存有两种:LruCacheDiskLruCache,LruCache用于实现内存缓存,DiskLruCache用于实现存储设备缓存

项目中的缓存使用

在一些项目中通常会结合使用内存缓存和存储设备缓存,比如第一次从网络中获取到图片,将图片缓存到存储设备缓存和内存缓存,当再一次加载图片时,会首先从内存缓存获取查找需要的图片是否存在(从内存缓存获取图片的速度最快),如果没有,会到存储设备缓存获取查找需要的图片是否存在,如果没有,最后才再一次从网络获取图片。

4.1 LruCache

内部采用 LinkedHashMap 以强引用的方式存储外界的缓存对象,提供 getput 方法完成缓存的获取和添加。

强引用:直接的对象引用。

软引用:当一个对象只有软引用存在时,系统内存不足时此对象才会被gc回收。

弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

4.1.1 使用LruCache实现内存缓存

private LruCache<String, Bitmap> mMemoryCache;

//设置缓存大小并计算bitmap大小
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
    }
};

//获取缓存对象
mMemoryCache.get(key);

//添加缓存对象
mMemoryCache.put(key, bitmap);

sizeOf() 方法的作用是计算缓存对象的大小,返回的大小单位要和总容量 cacheSize 单位一致。
以上代码设置了缓存总容量为进程可用内存的1/8,单位为KB,sizeOf()返回bitmap大小。

在一些特殊情况下,还需要重写LruCache的 entryRemoved() 方法,LruCache在移除旧缓存时会调用该方法,可以在entryRemoved 中完成一些回收工作。

4.2 DiskLruCache

https://github.com/JakeWharton/DiskLruCache

4.2.1 DiskLruCache的创建

DiskLruCache 的创建通过 open(File directory, int appVersion, int valueCount, long maxSize) 方法获取对象

  • directory:缓存目录路径。如果希望应用卸载后删除缓存文件,就选择sd卡上的缓存目录;如果要保留则选择sd卡上的其他特定目录。

  • appVersion:设置成1即可。当版本号改变时,之前的缓存文件会被清空,但大多数时候版本号更改缓存文件仍然有效。

  • valueCount:表示单个节点所对应的数据的个数,一般设为1即可。

  • maxSize:缓存的总容量。

private DiskLruCache mDiskLruCache;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB

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

4.2.2 DiskLruCache的缓存添加

详细操作步骤:

  • 通过图片的url将其转换成key(该key一般为md5,转为md5是防止url有特殊符号),通过key获取到Editor对象

  • 通过Editor对象获取文件输出流,将图片写入缓存文件中

  • 缓存完成后调用editor.commit()方法提交才正式写入为缓存,如果写入期间出现异常,调用editor.abort()回滚操作。

private static final int DISK_CACHE_INDEX = 0; //因为在DiskLruCache创建时open()方法中的valueCount设置为1,这里需要设置为0

String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
    if (downloadUrlToStream(url, outputStream)) {
        editor.commit();
    } else {
        editor.abort();
    }
    mDiskLruCache.flush();
}

//将网络下载的图片写入到缓存文件中
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {}

private String hashKeyFromUrl(String url) {
    String cacheKey;
    try {
        final MessageDigest mDigist = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch(Exception 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(OxFF & bytes[i]);
        if (hex.length == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

4.2.3 DiskLruCache的缓存查找

详细操作步骤:

  • 获取到图片url将其转换成key,通过key获取到 Snapshot 对象

  • 通过 Snapshot 对象获取到文件输入流就能读取到Bitmap对象(一般情况下不会去获取原始图片大小,会对获取的图片进行缩放,但使用 BitmapFactory.OptionsFileInputStream 的缩放会存在问题,原因是 FileInputStream 是有序的文件流,而缩放图片要两次调用 decodeStream() 方法,这会导致第二次 decodeStream() 时返回null。
    解决方法:通过文件流获取对应的文件描述符,再通过 BitmapFactory.decodeFileDescriptor() 方法加载获取缩放后的图片)

private ImageResizer mImageResizer = new ImageResizer();//自己封装的一个图片缩放封装类

Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
    FileInputStream fis = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
    FileDescriptor fd = fis.getFD();
    bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
    if (bitmap != null) {
        addBitmapToMemoryCache(key, bitmap);
    }
}

public class ImageResizer {
    public ImageResizer() {}

    private Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeFileDescriptor(fd, null, options);

        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }
} 
 类似资料: