【Android】LRU 与 Android 缓存策略
LRU 基本实现
LRU 缓存机制是一种常用的缓存策略,其选择最近最久未使用的条目予以淘汰。该算法赋予每个条目一个访问字段 key,用来记录该条目自上次被访问以来所经历的时间 t,当须淘汰某条目时,选择现有条目中其 t 值最大的,即最近最少使用的条目予以淘汰,不关心 t 具体值的情况可以通过链表的顺序插入来实现自然排序,访问字段和条目组成键值对。
LRU 的增改查操作往往要求在常数时间复杂度内进行,要实现顺序排列且在常数时间内删除过期条目,LRU 结构单元可以选择为双向链表,get 查找方法也要在常数时间内完成,所以 LRU 结构构成还要有哈希表的参与,基本的 LRU 结构是:
class Node<K, V>{K key;V value;Node<K, V> prev, next;
}int capacity;
Node<K, V> dummy;
Map<K, Node<K, V>> hm = new HashMap<>();
public int get(int key) {Node result = hm.getOrDefault(key, dummy);if (result != dummy) {moveToHead(result);}return result.value;
}public void put(int key, int value) {Node result = hm.get(key);if (result != null) {moveToHead(result);result.value = value;} else {result = new Node(key, value);result.prev = dummy;result.next = dummy.next;dummy.next.prev = result;dummy.next = result;hm.put(key, result);}if (hm.size() > capacity) {Node del = dummy.prev;del.prev.next = dummy;dummy.prev = del.prev;hm.remove(del.key);}
}public void moveToHead(Node result) {result.prev.next = result.next;result.next.prev = result.prev;result.prev = dummy;result.next = dummy.next;dummy.next.prev = result;dummy.next = result;
}
LruCache
LruCache<K, V> 作为泛型类,内部采用 LinkedHashMap 存储外界缓存对象,通过 get 和 put 方法完成缓存获取和添加操作,初始化过程要提供缓存的总容量大小和重写 sizeOf 方法,sizeOf 方法用来计算缓存对象的大小。
int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
/* 缓存总容量大小为当前进程可以用内存的 1/8 */
LruCache<String, Bitmap> cache = new LruCache<>(cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap bitmap) {return bitmap.getRowBytes() * bitmap.getHeight() / 1024;}
}cache.get(key);
cache.put(key, bitmap);
源码分析
首先来到 get 方法的执行过程,因为 LinkedHashMap 本身线程不安全,所以使用 synchronized 关键字做同步查询,如果有值则 hitCount 命中值自增且就地返回该值,若没有值则 missCount 自增。
if (key == null) {throw new NullPointerException("key == null");
}V mapValue;
synchronized (this) {mapValue = map.get(key);if (mapValue != null) {hitCount++;return mapValue;}missCount++;
}
若没有值则尝试通过 create 方法获得值,该方法默认返回 null,可以重写 create 方法在查询不到值的时候做加载,是懒加载策略的体现,接下来同步尝试将加载的 value 写入哈希表,最后会判断是否有覆写行为,如果有则回调 entryRemoved 方法,该方法系回调监听器,供开发者对旧值做资源释放等操作;若无则调用 trimToSize 方法尝试裁剪超出容量的旧条目。
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/V createdValue = create(key);
if (createdValue == null) {return null;
}synchronized (this) {createCount++;mapValue = map.put(key, createdValue);if (mapValue != null) {// There was a conflict so undo that last put
map.put(key, mapValue);} else {size += safeSizeOf(key, createdValue);}
}if (mapValue != null) {entryRemoved(false, key, createdValue, mapValue);return mapValue;
} else {trimToSize(maxSize);return createdValue;
}
我们来到 trimToSize 方法,其实现比较简单,首先会检查当前容量 size 是否大于 maxSize,是则调用 eldest 方法获得 LinkedHashMap 表尾的键值对条目,接下来从哈希表中删除该条目并更新缓存大小,最后统计被淘汰的条目数量,触发 entryRemoved 回调。
public void trimToSize(int maxSize) {while (true) {K key;V value;synchronized (this) {if (size < 0 || (map.isEmpty() && size != 0)) {throw new IllegalStateException(getClass().getName()+ ".sizeOf() is reporting inconsistent results!");}if (size <= maxSize) {break;}Map.Entry<K, V> toEvict = eldest();if (toEvict == null) {break;}key = toEvict.getKey();value = toEvict.getValue();map.remove(key);size -= safeSizeOf(key, value);evictionCount++;}entryRemoved(true, key, value, null);}
}
DiskLruCache
DiskLruCache 用于实现存储设备缓存,通过将缓存对象写入文件系统实现缓存效果,该类不属于 Android SDK 的内容,所以要引入依赖:implementation 'com.jakewharton:disklrucache:2.0.2',创建方式是通过 open 方法,其参数 directory 表示期望磁盘缓存在文件系统的缓存路径,appVersion 类似 DatabaseHelper 的 update 机制,appVersion 发生改变时会清空之前所有的缓存文件,valueCount 表示单个节点对应的数据个数,maxSize 表示缓存的总大小。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)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 表示缓存条目的可编辑视图,可以通过 DiskLruCache 实例的 edit(key) 方法获取对应的 Editor 实例。通过该实例可以获取输出流,向缓存写入数据。写入操作产生的所有字节数据对应创建 Editor 时指定的 key 值。若要删除缓存条目,可以调用 remove(key) 方法将其清除。值得注意的是,DiskLruCache 不允许同时编辑相同的缓存对象,如果该缓存正在被编辑,则 edit 会返回 null。
String key = "bitmap_key";
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {OutputStream os = editor.newOutputStream(0);try {os.write("Hello DiskLruCache".getBytes());editor.commit();} catch (IOException e) {editor.abort(); // 回退操作e.printStackTrace();} finally {try { os.close(); } catch (IOException ignored) {}}
}
缓存查找要借助表示缓存条目只读视图的 Snapshot 类,逻辑和 Editor 类似,可以通过 DiskLruCache 实例的 get(key) 方法获取对应的 Snapshot 实例。通过该实例可以获取输入流,读取缓存中的数据。读取操作不会修改缓存条目,获取的数据对应创建时指定的 key 值。使用完成后需要调用 snapshot.close() 关闭流,释放资源。
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot != null) {InputStream is = snapshot.getInputStream(0);BufferedReader reader = new BufferedReader(new InputStreamReader(is));StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}String cachedValue = sb.toString();snapshot.close();Log.d("DiskLruCache", "Cached value: " + cachedValue);
}
Bitmap
Bitmap 在 Android 指 1 幅图片资源,可以通过 BitmapFactory 的 decodeFile、decodeResource、decodeStream 和 decodeByteArray 方法分别从文件系统、资源、输入流和字节数组加载 Bitmap 对象,最终会调用 BitmapFactory 的 native 层方法。
大多数情况 ImageView 显示图片的内容区域要小于图片本身的大小,所以将图片完整的加载到设备再使用 scaleType 属性进行缩放或裁切往往是不高效的,我们可以采用 BitmapFactory.Options 来加载适应尺寸的图片,Options 有 inSampleSize 参数表示采样率,inSampleSize 会重复作用于 Bitmap 的宽高,所以缩放比例是 1/inSampleSize^2。
Options 的 inJustDecodeBounds 参数表示是否仅获取图片信息,所以我们可以通过获取图片宽高信息后结合 ImageView 显示区域的尺寸计算出采样率 inSampleSize,使用该采样率加载 Bitmap。
public static Bitmap suitableBitmap(Resources res, int resId, int weith, int height) {final BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(res, resId, options);options.inSampleSize = calculateInSampleSize(options, weith, height);/* calculateInSampleSize 获得采样率过程略 */options.inJustDecodeBounds = false;return BitmapFactory.decodeResource(res, resId, options);
}
