图片三级缓存:LruCache的原理

1,136 阅读5分钟

概述

图片的加载一直是Android开发中重要的一环,处理不当的话很容易就会造成OOM,尤其是当我们需要使用大量图片的时候,图片的缓存技术就至关重要了。

图片的缓存技术分为三级: 

1.内存缓存 : 读取速度最快

技术: LruCache

2.本地缓存 : 读取速度低于内存缓存

技术: DiskLruCache

3.网络缓存 : 读取速度最慢

技术: 保存在服务器中

图片的缓存流程: 图片需要从缓存中获取的时候,先从内存缓存中获取,如果没有的话,再从本地缓存中获取,如果还没有的话,就只能从网络中去获取。

本文主要讲解LruCache的实现和原理。

为什么会出现LruCache?

由于 Android 为每个进程分配的可用内存都是有限的,如果进程使用的内存超过了所分配的限制就会出现内存溢出问题。同时,如果应用中每个资源都需要从本地或网络加载,这会影响到应用的性能,为了保证应用性能又避免内存溢出,于是出现内存缓存技术。

LruCache的优点

是为了应用能够更有效的去管理内存,保证了缓存是处于一种可控的状态,有效的防止了OOM的出现。

使用场景

一般是使用于图片的缓存,缓存技术有助于减少图片的网络请求,也有助于用户体验。

基本使用

初始化:

int maxMemory = (int) (Runtime.getRuntime().totalMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String,Bitmap> cache = new LruCache<String,Bitmap>(cacheSize) {

  // 计算当前的Bitmap的内存大小,默认是返回 1。
  @Override
  protected int sizeOf(String key, Bitmap value) {
     return value.getByteCount() / 1024;
  }

  // 如果您的缓存值包含需要显式释放的资源,请重写此方法
  @Override
  protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
     super.entryRemoved(evicted, key, oldValue, newValue);
  }
};

put方法:

cache.put("key", bitmap);

get方法:

cache.get("key");

remove方法:

cache.remove("key");

缓存原理

其实就是维护了一个缓存对象列表,将没有访问过的对象都放在队尾,将最近添加和最近使用过的对象放在队头,当需要释放缓存的时候,从队尾开始剔除对象。

(图片来自网络,侵删)

源码分析

获取对象

   public LruCache(int maxSize) {
       if (maxSize <= 0) {
           throw new IllegalArgumentException("maxSize <= 0");
       }
       this.maxSize = maxSize; //设置缓存的大小
       this.map = new LinkedHashMap<K, V>(0, 0.75f, true); // 保存需要缓存的对象
   }

可以从它的构造参数看出,是通过LinkedHashMap 来保存需要缓存的数据。

为什么要用LinkedHashMap来维持缓存呢?

LinkedHashMap 是由数组 加 双向链表的数据结构来实现。 其中的双向链表的结构可以实现 访问顺序 和 插入顺序。

LinkedHashMap 的构造参数:

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

其中 accessOrder 设置为true是访问顺序,false为插入顺序。

让我们来看一个例子:

   public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0,0.75f,true);
        map.put(0, 0);
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        map.put(4, 4);
        map.put(5, 5);
        map.put(6, 6);
        map.get(1);
        map.get(2);

        for(Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
    }

结果:

0:0
3:3
4:4
5:5
6:6
1:1
2:2

可以看到,最近使用的 12 都放在了表头。

这就满足了LRU缓存算法的思想,所以LinkedHashMap会被LruCache使用。

现在让我们来具体看看LruCache的put、get、remove方法。

put方法


会将添加的对象放在队列的第一个位置

    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }


        V previous; // 如果map中存在相同的数据,那么这个就是map中那个相同的数据
        synchronized (this) {
            putCount++; // 添加的数量加一
            size += safeSizeOf(key, value); // 将内存的大小增大
            previous = map.put(key, value); // 将 新的数据添加到map的头部中,如果map中存在,那么旧数据会被移除,也就是previous。
            if (previous != null) { // 旧数据移除,就需要减去旧数据的大小
                size -= safeSizeOf(key, previous);
            }
        }


        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }


        trimToSize(maxSize);
        return previous;
    }

让我们来看看 safeSizeOf 方法:

    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value); // 这里是我们重写的方法,就是需要保存的对象的大小
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

再来看看trimToSize这个方法:

这个方法就是当内存大小超过设定大小的时候,去移除部分数据直到内存大小 < 设定大小。

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 = map.entrySet().iterator().next()
                              // 这是取出队尾的元素
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }


                key = toEvict.getKey();
                value = toEvict.getValue();
                // 移除 队尾的元素,并更新缓存的大小
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }


            entryRemoved(true, key, value, null);
        }
    }

可以看到代码中,循环的去删除多出来的对象,直到当前的缓存大小小于或者等于设定大小。

再来看看entryRemoved方法:

这个方法使用的时候需要被重写,先来看看他的四个参数的含义:

evicted

true 表示 数据是 trimToSize 移除的,为了释放空间, false 表示 是 put 和remove 主动移除的

key

就是map的key

oldValue

被移除的旧的数据

newValue

保存的新的数据,如果没有,则是null

get方法


public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }


        V mapValue;
        synchronized (this) {
            // map的get方法会使得map中的元素移动到头部
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        /**
         如果缓存取不到数据的话,可以创建一个数据,create()方法默认返回null 
         需要对它进行重写
         */
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }


        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            // 当 map中存在创建的数据的时候,将旧数据重新的put到map的表头中。
            if (mapValue != null) {
                map.put(key, mapValue);
            } else { // 否则就是增加新的数据
                size += safeSizeOf(key, createdValue);
            }
        }


        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

get方法使用的时候,会将被使用的对象移动到 队列的表头。

remove方法


    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }


        V previous;
        synchronized (this) {
                     // 在map中移除,并减少 缓存的大小
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }


        if (previous != null) {
            entryRemoved(false, key, previous, null);
        }


        return previous;
    }

结语

以上便是LruCache的原理,它的思想也是非常容易理解的,LruCache中的关键在于LinkedHashMap,如果有时间的话,可以去看看LinkedHashMap的实现。