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

LruCache的使用

哈泰
2023-12-01

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 也会执行压缩,删除之前的状态。

 类似资料: