1、Lru算法
LRU:Least Recently Used,即最近最少使用,意思是当缓存到达限制时候,优先淘汰近期内最少使用的缓存。
1.1 Lru的一种简单实现
使用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);
}
}
1.2Lru算法的另一种实现
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;
}
}
}
2、LruCache和DiskLruCahce在android中的使用
在Android中Bitmap数据量比较大,可以考虑LruCache、DiskLruCache缓存做二级缓存,减少请求量;同时,在一些经常请求且重要的数据,也可以放在这两个缓存中,在请求失败或请求错误时直接使用缓存的值,颇有些redis的味道。
2.1 LruCache的使用
2.1.1 初始化
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();
}
2.1.2 put/get操作
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;
}
2.2 DiskLruCache的使用
2.2.1 创建 DiskLruCache 对象
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()
关闭
2.2.2 写入DiskLruCache
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之前的状态。
2.2.3 读取DiskLruCache
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。
2.2.4 DiskLruCache原理
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 的数据,有四种状态:
- DIRTY:表示正在写入
- CLEAN:表示就绪,可以读取到最新的修改了
- REMOVE:表示被删除了
- READ:表示正在读取
接下来每一行,如:
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
对应
[状态] [key] [缓存文件1的size] [缓存文件2的size]
Entry 数据并不是在记录的位置“原地”修改,而是不停地添加新的状态到文件末尾,只有读取到最新的一条 Entry 相关的记录才能知道它的最新状态。随着操作越来越多,DiskLruCache 也会执行压缩,删除之前的状态。