LRU:Least Recently Used,即最近最少使用,意思是当缓存到达限制时候,优先淘汰近期内最少使用的缓存。
使用Java容器LinkedHashMap,LinkedHashMap本身就具有LRU算法的特性:
class LRUCache {
private Map<Integer, Integer> cacheMap = null;
public LRUCache(int capacity) {
// 参数设置true,当removeEldestEntry()返回true,则删除最旧的数据
cacheMap = new LinkedHashMap<Integer, Integer>(capacity,0.75F,true){
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
};
}
public int get(int key) {
return cacheMap.getOrDefault(key, -1);
}
public void put(int key, int value) {
cacheMap.put(key, value);
}
}
HashMap+双向链表:
class LRUCache2 {
// 当前缓存容量
private int size;
// 限制最大缓存容量
private int capacity;
// 定义伪头尾节点
private Node head;
private Node tail;
// 定义HashMap存储数据
private Map<Integer, Node> cache = new HashMap();
// 初始化操作
public LRUCache2(int capacity) {
size = 0;
this.capacity = capacity;
// 初始化头尾节点
head = new Node();
tail = new Node();
// 让头尾节点相联
head.next = tail;
tail.pre = head;
}
public int get(int key) {
Node node = cache.get(key);
// 不存在返回-1
if (null == node) {
return -1;
}
// 存在返回值,并且将当前节点移动到头
moveNodeHead(node);
return node.value;
}
public void put(int key, int value) {
Node node = cache.get(key);
// 不存在则插入,插入后判断当前容量是否大于限制最大容量
if (null == node) {
Node newNode = new Node(key, value);
cache.put(key, newNode);
// 放入链表头部
addNodeHead(newNode);
size++;
if (size > capacity) {
// 删除尾结点
Node tail = removeNodeTail();
cache.remove(tail.key);
}
} else {
// 存在则覆盖value,并且将当前节点移动到头
node.value = value;
moveNodeHead(node);
}
}
// 放入链表的头部
private void addNodeHead(Node node) {
// 当前头节点的下一个节点的pre和当前节点连接
head.next.pre = node;
//
node.next = head.next;
//
head.next = node;
node.pre = head;
}
// 将节点移动到链表的头部
private void moveNodeHead(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
addNodeHead(node);
}
// 删除链表的尾结点
private Node removeNodeTail() {
Node tailNode = tail.pre;
tailNode.pre.next = tailNode.next;
tailNode.next.pre = tailNode.pre;
return tailNode;
}
// 定义一个双向链表,实际的缓存
class Node {
private int key;
private int value;
private Node pre;
private Node next;
public Node() {
}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
}
在Android中Bitmap数据量比较大,可以考虑LruCache、DiskLruCache缓存做二级缓存,减少请求量;同时,在一些经常请求且重要的数据,也可以放在这两个缓存中,在请求失败或请求错误时直接使用缓存的值,颇有些redis的味道。
public LruCache<String, Bitmap> mMemoryCache;
private void initMemoryCache() {
ActivityManager am = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
int availMenInBytes = am.getMemoryClass() * 1024 * 1024;
this.mMemoryCache = new LruCache<String, Bitmap>(availMenInBytes / 8) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return getBitmapSize(bitmap);
}
};
}
//根据android版本来计算bitmap的实际占用内存
public int getBitmapSize(Bitmap bitmap) {
if (bitmap == null) {
return 0;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//API 19
return bitmap.getAllocationByteCount();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
//API 12
return bitmap.getByteCount();
}
// 在低版本中用一行的字节x高度
return bitmap.getRowBytes() * bitmap.getHeight();
}
private void addToMemoryCache(String key, Bitmap bitmap) {
if (getFromMemCache(key) == null) {
if ((key != null) && (bitmap != null)) {
try {
this.mMemoryCache.put(key, bitmap);
} catch (Exception e) {
LogUtils.e(e.toString());
}
}
}
}
private Bitmap getFromMemCache(String key) {
Bitmap bm = mMemoryCache.get(key);
return bm;
}
private DiskLruCache mDiskCache;
private void initDiskCache() {
File localFile = FileUtils.getCacheDir(UtilServices.getContext().getApplicationContext(), DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(localFile);
}
private class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
private InitDiskCacheTask() {}
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
try {
File cacheDir = params[0];
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
mDiskCache = DiskLruCache.open(cacheDir, getVersionCode(), 1, DISK_MAX_SIZE);
mDiskCacheStarting = false;
mDiskCacheLock.notifyAll();
} catch (IOException localIOException) {
LogUtils.e(localIOException.getMessage());
}
}
return null;
}
}
构造方法是 private 修饰的,无法使用。使用静态方法 static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
来创建DiskLruCache 对象。
参数 directory 表示保存的目录,注意外部目录权限问题
参数 appVersion 可以设置为版本号 version code。
参数 valueCount 表示一个 key 可以关联几个文件,一般为 1(一般情况关联多个没有必要,而且会增加编码复杂度)
参数 maxSize 缓存大小限制,单位 Byte
需要调用close()
关闭
private DiskLruCache.Editor editor(String key) {
if (mDiskCache != null) {
try {
DiskLruCache.Editor edit = mDiskCache.edit(key);
return edit;
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public void put(String key, String value) {
DiskLruCache.Editor editor = null;
BufferedWriter writer = null;
OutputStream os = null;
try {
editor = editor(key);
if (editor == null) {
return;
}
os = editor.newOutputStream(0);
writer = new BufferedWriter(new OutputStreamWriter(os));
writer.write(value);
writer.flush();
editor.commit();
} catch (IOException e) {
Log.e(e.toString());
try {
if (editor != null) {
editor.abort();
}
} catch (Exception e1) {
Log.e(e1.toString());
}
} finally {
writer.close();
os.close();
}
}
先调用 DiskLruCache.Editor edit(String key)
方法获取一个 DiskLruCache.Editor
对象,再通过这个对象进行写入操作。
返回的类型是 DiskLruCache.Editor,其实就是将写入相关的一些操作抽象处理,对这个对象的操作都对应 key关联的缓存文件。
如果同时有另一个 Editor 对象是通过 key 获取的,edit 方法将返回 null。保证同时只有一个 Editor 对象在对同一个 key 进行写入操作。因此调用之后需要判断一下。
使用 OutputStream newOutputStream(int index)
创建输出流来写入数据,注意这个流不是缓存流,如果需要缓存流可以创建 BufferedOutputStream
包装一下 OutputStream。
这个 OutputStream 需要手动关闭。
除了关闭输出流,还还需对 Editor 设置结果。如果写入操作和相关业务成功了,缓存文件有效,则调用 Editor.commit()
方法表示缓存写入成功。如果写入操作或相关业务失败了,缓存文件无效,则调用 Editor.abort()
来还原为未获取 Editor之前的状态。
public final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
continue;
} catch (Exception e) {
Log.e(e.toString());
}
}
if (mDiskCache != null) {
try {
DiskLruCache.Snapshot snapShot = mDiskCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);// 0表示第一个缓存文件,不能超过valueCount
return BitmapFactory.decodeStream(is);
}
} catch (Exception e) {
Log.e(e.toString());
}
}
}
return null;
}
先调用 DiskLruCache.Snapshot get(String key)
获取一个 DiskLruCache.Snapshot对象,再通过这个对象进行读取操作。
返回的类型是 DiskLruCache.Snapshot,其实就是这个 key对应的相关数据,主要是多个文件的输入流和大小,多个文件的数量对应构造方法里的 valueCount。
Snapshot.getLength(int index)
获取文件大小,进行一些判断。
Snapshot.getInputStream(int index)
获取一个InputStream
,可以用来读取文件内容,注意这个流不是缓存流,如果需要缓存流可以创建 BufferedInputStream
包装一下 InputStream
。
这个 InputStream 需要手动关闭,既可以直接关闭 InputStream,也可以调用 Snapshot.close()
来关闭属于它的所有 InputStream。
DiskLruCache在本地有一个journal的日志文件:
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
其中1表示diskCache的版本,100表示应用的版本,2表示一个key对应多少个缓存文件。journal 文件还记录了每个 Entry 的数据,有四种状态:
接下来每一行,如:
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
对应
[状态] [key] [缓存文件1的size] [缓存文件2的size]
Entry 数据并不是在记录的位置“原地”修改,而是不停地添加新的状态到文件末尾,只有读取到最新的一条 Entry 相关的记录才能知道它的最新状态。随着操作越来越多,DiskLruCache 也会执行压缩,删除之前的状态。