Android进阶知识(二十六):Android中的缓存策略

Android进阶知识(二十六):Android中的缓存策略

  缓存策略在Android中有着很广泛的使用场景,尤其在图片加载的情景下,缓存策略更为重要。在图片加载的情景下,缓存的目的在于提高程序的效率,同时解决不必要的流量开销的问题
  缓存策略没有统一的标准,其主要包括缓存的添加、获取和删除三类操作。那缓存为何需要删除呢?
在这里插入图片描述
  关键在于不管是内存缓存还是存储设备缓存,它们的缓存大小都是有限制的,当缓存容量已满,但是程序还需要添加缓存,这就需要删除旧的缓存添加新的缓存,而如何定义缓存的新旧就是一种策略,不同的策略对应着不同的缓存算法。
  常用的一种缓存算法为LRU,LRU是近期最少使用算法,核心思想为当缓存已满时,会优先淘汰那些近期最少使用的缓存对象。
在这里插入图片描述
  采用LRU算法的缓存有两种:LruCache(实现内存缓存)和DiskLruCache(存储设备缓存)

一、LruCache

  LruCache是Android 3.1所提供的一个泛型缓存类,通过support-v4兼容包可以兼容到早期版本。LruCache内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存已满时,LruCache会移除较早使用的缓存对象,然后添加新的缓存对象
  关于强引用、软引用和弱引用的区别见下表。

引用描述
强引用直接的对象引用
软引用当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收
弱引用当一个对象只有弱引用存在时,此对象会随时被gc回收

  另外,LruCache是线程安全的。LruCache的定义如下。

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

  以图片缓存为例子,LruCache的典型初始化代码如下。LruChache的初始化只需要提供缓存的总容量大小以及重写sizeOf方法即可,其中sizeOf方法作用是计算缓存对象的大小

// 当前进程可用内存
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;
    }
};

  特殊情况下需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用该方法,可以在该方法中完成一些资源回收工作。LruCache的缓存添加、获取以及删除都比较简单。

mMemoryCache.put(key, bitmap);

mMemoryCache.get(key);

mMemoryCache.remove(key);

在这里插入图片描述

二、DiskLruCache

  DiskLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存效果。DiskLruCache不属于Android SDK的一部分,可以从如下网站获取源码。

https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java

  值得一提的是,DiskLruCache源码获取后不能直接在Android中使用,需要稍微修改编译错误。下面笔者将介绍DiskLruCache的创建、缓存查找以及缓存添加。

  1. DiskLruCache的创建

  DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,如下所示。

public static DiskLruCache open(File directory, int appVersion,
                                int valueCount, long maxSize)

  其中,open方法的四个参数具体描述如下表。

参数描述
File directory表示磁盘缓存在文件系统中的存储路径。路径可以选择SD卡上的缓存目录(/sdcard/Android/data/package_name/cache),当应用卸载后,此目录会被一并删除
int appVersion表示应用的版本号,当版本号发生改变时候DiskLruCache会清空之前所有的缓存文件,由于实际应用意义不大,因此一般设置为1
int valueCount表示单个节点所对应的数据的个数,一般设定为1
long maxSize表示缓存的总大小,当缓存大小超出这个值,DiskLruCache会清除一些缓存以保证总大小不大于该值

  典型的DiskLruCache的创建代码如下所示。

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);
  1. DiskLruCache的缓存添加

  DiskLruCache的缓存添加操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。缓存的添加分为4步,以下以图片为例子。

  1) 创建key
  由于图片的url可能有特殊字符,这将影响url在Android中的使用,因此需要通过hashKeyFormUrl方法将图片url转换成md5值的key。

String key = hashKeyFormUrl(url);

  2) 根据key获取Editor对象,并获取文件输出流
  对于当前key来说,如果当前不存在其他Editor对象,那么editor()返回一个新的Editor对象,如果这个缓存正在被编辑,那么editor()返回null。DiskLruCache不允许同时编辑一个缓存对象

DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}

  其中由于前面设置了DiskLruCache的open方法设置了一个节点只有一个数据,因此这里DISK_CACHE_INDEX设置为0即可。
在这里插入图片描述

  3) 将数据通过文件输出流写入文件系统
  有了文件输入流之后,当从网络下载图片时,通过该输出流将图片写入文件系统。简略代码如下所示。

out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != null) {
    // in为网络下载图片的输入流
    out.write(b);
}

  4) 通过Editor的commit()提交写入操作
  通过文件输出流写入文件系统,并没有真正地将图片写入,需要通过Editor的commit()提交写入操作如果图片下载过程发生异常,那么可以通过Editor的abort()方法回退整个操作

// downloadUrlToStream方法用于下载图片并写入文件输出流
// 返回true下载正常,false下载异常
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
    editor.commit();
} else {
    editor.abort();
}
mDiskLruCache.flush();
  1. DiskLruCache的缓存查找

  缓存查找过程过程为:首先根据key通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象获得缓存的文件输出流,从而得到Bitmap对象

Bitmap bitmap = null;
String key = hashKeyFromUrl(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);
   }                    
}

  如上代码,为了避免加载图片导致OOM,通过BitmapFactory.Options对图片进行了缩放——方法decodeSampleBitmapFromFileDescriptor(高效加载Bitmap在笔者的前一篇笔记:Android进阶知识(二十五):Bitmap简介及其高效加载中介绍了)。
  另外,FileInputStream是一种有序的文件流,高效加载需要两次调用decodeStream,这会影响文件流的位置属性,导致第二次decodeStream得到的是null。为了解决这个问题,可以通过文件流来得到所对应的文件描述符,再进行缩放加载。这也是笔者在前一篇笔记中提到的decodeStream进行高效加载比较特殊的原因。
在这里插入图片描述
  至此DiskLruCache的创建、缓存的添加以及查找过程就介绍完了,除此之外,DiskLruCache还提供了remove、delete等方法用于磁盘缓存的删除操作,这里就不多介绍了。

三、ImageLoader实现思想

  笔者在Android进阶知识(二十五):Bitmap简介及其高效加载中介绍了Bitmap的高效加载方式,以及前面介绍了LruCache和DiskLruCache的使用,有了这些知识那么对于我们常见的图片加载第三方库ImageLoader,读者就可以进行实现了。
  一个优秀的ImageLoader应该具备如下的功能:

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

  内存缓存和磁盘缓存是ImageLoader的核心,通过这两级缓存极大地提高了程序的效率并且有效地降低了对用户所造成的流量消耗,只有当这两级缓存不可用才需要从网络拉取。
在这里插入图片描述
  一个需要注意的是,ListView或者GridView中View的复用有一个缺点:假设itemA对应ImageViewA,如果进行快速滑动,那么itemB复用了ImageViewA,由于网络加载图片尚未下载好A图片,导致显示时候itemB显示A图片。
  为了解决由于View复用所导致的列表错位问题,在给ImageView设置图片之前可以检测它的url是否改变,改变不设置
在这里插入图片描述
  对于列表卡顿的优化,解决思想是不要在主线程中做太耗时的操作即可提高滑动的流畅度,从三个方面可以说明这个问题:异步方式处理耗时操作;控制异步任务执行频率;开启硬件加速
在这里插入图片描述

参考资料:《Android开发艺术探索》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值