LruCache源码完全解析

前言

关于android内存管理机制,之前一直听说,从没有静下心来,认真看过源码,只能人云亦云,完全不知其内部如何实现,作为一个开发人员来说,实在是要不得,所以本着求知的心态,分析一下android内部缓存的实现机制,也希望能帮助到对android内存缓存不太熟悉的同学。

概述

Android自带的内存缓存机制是:LRU:Least Recently Used,即:最近最少使用算法,说白了就是,最近最少使用的内存都会被释放。对应的类即为:LruCache.java

源码解析

成员变量,构造方法

//重点关注对象,内存的操作,均是由此map完成
 private final LinkedHashMap<K, V> map;

    /** Size of this cache in units. Not necessarily the number of elements. */
    //当前内存大小
    private int size;
    //内存最大值
    private int maxSize;

    private int putCount;
    private int createCount;
    private int evictionCount;
    private int hitCount;
    private int missCount;

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    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);
    }

我们的重点关注变量是map,size,maxSize,因为,size是当前内存空间的大小,maxSize决定了内存的最大空间,map是操作内存实现的集合。

LinkedHashMap简单介绍

在LruCache的构造方法中,出现了LinkedHashMap,那就多扯一点,稍稍扩展一下LinkedHashMap。

  /**
     * Constructs a new {@code LinkedHashMap} instance with the specified
     * capacity, load factor and a flag specifying the ordering behavior.
     *
     * @param initialCapacity
     *            the initial capacity of this hash map.
     * @param loadFactor
     *            the initial load factor.
     * @param accessOrder
     *            {@code true} if the ordering should be done based on the last
     *            access (from least-recently accessed to most-recently
     *            accessed), and {@code false} if the ordering should be the
     *            order in which the entries were inserted.
     * @throws IllegalArgumentException
     *             when the capacity is less than zero or the load factor is
     *             less or equal to zero.
     */
    public LinkedHashMap(
            int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        init();
        this.accessOrder = accessOrder;
    }

从LinkedHashMap的构造方法的注释中,不难理解各个参数的意思,首先

  • initialCapacity:初始map集合的大小
  • loadFactor:装载因子,越大空间利用率越高,同时查询速度也会降低,默认值0.75
  • accessOrder true:即集合按照元素的最近访问排序,false:按照元素的插入顺序排序

详细了解LinkedHashMap,请参考此文

这就很好理解LruCache的构造方法了,在其中构造了一个按照元素的最近访问排序的LinkedhashMap,初始大小为0,并赋予了最大空间maxSize。

下面逐一介绍几个主要的方法:

safeSizeOf(),sizeOf()

 private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }


    protected int sizeOf(K key, V value) {
        return 1;
    }

这两个方法其实就是计算当前Size大小的,从源码中可以看到,sizeOf一直返回的是1,在safeSizeOf中,当result<0的时候,会抛出异常。

Warning:这里需要特别注意,sizeOf 这个方法是我们熟悉的,一般使用 LruCache 都会重写这个方法返回每条数据的实际大小。为什么要重写呢? 因为这个方法默认的实现是返回 1。这样的话,size 相当于记录的是缓存数据的条数,而这可能并不是我们想要的

trimToSize(int maxSize)

 private 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;
                }

                // BEGIN LAYOUTLIB CHANGE
                // get the last item in the linked list.
                // This is not efficient, the goal here is to minimize the changes
                // compared to the platform version.
                Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

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

这个方法内部是一个无限循环,删除 map 里面最久未使用的,然后更新 size,如果 size 小于 maxSize 就跳出循环。

put(K key, V value)

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

        V previous;
        synchronized (this) {
            putCount++;
            //cache_size+1
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            //存在相同的key,cache_size-1,恢复
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //删除最近不使用的
        trimToSize(maxSize);
        return previous;
    }

此时,再看put方法就比较好理解了,即:如果 key 或者 value 为空会抛出异常,否则在同步块中进行添加操作。首先是 putCount 加一,然后调用 safeSizeOf 方法增加 size,接着把数据放到 map 中,如果这个 key 已经存放了数据,那么应该减去这条数据的大小,因为它已经被覆盖调了。同步块结束后,如果确实覆盖了数据,会调用 entryRemoved,这个方法默认是空,什么也没做,我们自己创建 LruCache 时可以选择重写。最后还需要调用 trimToSize,这个方法用来防止数据超出 maxSize。

get(K key)


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

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            //若存在,直接返回
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        /*
         * 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.
         */
        //如果为null,直接返回null,
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }
        //多线程操作时,
        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);
            //如果存在对应key_value,则释放createValue,并还原mapValue
            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                //新增,cache_size+1
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            //返回
            return mapValue;
        } else {
            //删除最近不用的
            trimToSize(maxSize);
            return createdValue;
        }
    }

最后才关注get方法,因为这个代码最长。
key 依然不能为空,然后就是从 map 中取数据,递增hitCount,最后直接返回数据。这是成功找到缓存的情况,如果找不到还会执行下面的代码。下面的逻辑是调用 create 创建 value。create需要我们自己重写,默认返回 null,所以默认情况下找不到缓存就返回 null。 如果重写了 create 那么接着会把新建的数据加入 map,并且增加 size,执行 trimToSize 等操作

其它操作

除了 get 和 put,其它还有一些操作。比如 evictAll() 用于清除所有缓存,size() 返回 size 大小,一系列的以 Count 结尾的方法用于返回 hitCount 等计数值的大小。这些代码都比较简单,没什么好说的。

总结

LruCache 实现了数据的内存缓存,可以看出整体思路并不是很复杂,关键在于使用了 LinkedHashMap。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值