LruCache源码浅析

移动设备开发中,由于移动设备(手机等)的内存有限,所以使用有效的缓存技术是必要的。android提供来一个缓存工具类LruCache,开发中我们会经常用到,下面我们就具体分析一下LruCache。

LruCache缓存数据是采用持有数据的强引用来保存一定数量的数据的。每次用到(获取)一个数据时,这个数据就会被移动(一个保存数据的)队列的头部,当往这个缓存里面加入一个新的数据时,如果这个缓存已经满了,就会自动删除这个缓存队列里面最后一个数据,这样一来使得这个删除的数据没有强引用而能够被gc回收。

1. 首先我们先看一下LruCache的构造函数:

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

1.1 创建LruCache时需要传入一个maxSize,这表示LruCache规定的最大存储空间。这里我想问一下maxSize是指缓存数据对象的个数呢,还是缓存数据所占用的内存总量呢?

其实都可以,可以是缓存数据的个数,也可以使缓存数据所占用内存总量,当然也可以是其他.到底是什么,需要看你的LruCache如何重写这个方法:sizeOf(K
key, V value),我们看一下LruCache的sizeOf函数源码:

protected int sizeOf(K key, V value) {//子类覆盖这个方法来计算出自己的缓存对于每一个保存的数据所占用的量
    return 1;//默认返回1,这说明:默认情况下缓存的数量就是指缓存数据的总个数(每一个数据都是1).
}

那如果我使用LruCache来保存bitmap的图片,并且希望缓存的容量是4M那这么做,参考代码如下:

int cacheSize = 4 * 1024 * 1024; // 4MiB
new LruCache<String, Bitmap>(cacheSize) {
<span style="white-space:pre">	</span>@Override
<span style="white-space:pre">	</span>protected int sizeOf(String key, Bitmap bitmap) {//计算每一个缓存的图片所占用内存大小
<span style="white-space:pre">		</span>return bitmap.getRowBytes() * bitmap.getHeight();
<span style="white-space:pre">	</span>}
};

1.2  LruCache构造函数中我们注意到了LinkedHashMap这个类,我们知道LinkedHashMap是保存一个键值对数据的,并且可以维护这些数据相应的顺序的,LinkedHashMap初始化的源码如下:

//调用HashMap的构造方法来构造底层的数组  
    public LinkedHashMap(int initialCapacity, float loadFactor) {  
        super(initialCapacity, loadFactor);  
        accessOrder = false;    //链表中的元素默认按照插入顺序排序  
    }

其中initialCapacity表示加载加载因子,在HashMap扩容时会用到。另外一个参数就是loadFactor,LinkedHashMap内部维持了一个双向循环链表,链表的排序有两种,用loadFactor参数的值进行区分,当loadFactor为false时,表示按照插入顺序排序,当loadFactor为true时,标志按照访问顺序排序。LruCache中LinkedHashMap的构造函数传入了true,这实现保存的数据是有一定顺序的,它是按访问顺序排序排序的,使用过一个存在的数据,这个数据就会被移动到数据队列的头部。

想要具体了解LinkedHashMap,请参见LinkedHashMap源码剖析:http://blog.csdn.net/ns_code/article/details/37867985

2. LruCache如何、何时判断是否缓存已经满来,并且需要移除不常用的数据呢?

在LruCache里面有一个方法:trimToSize()就是用来检测一次当前是否已经满,如果满来就自动移除一个数据,一直到不满为止:

public void trimToSize(int maxSize) {//默认情况下传入是上面说的最大容量的值 this.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.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
    }
}

上面的源码中我给出了说明,很好理解。这里要注意的是trimToSize这个方法是public的,说明其实我们自己可以调用这个方法的。那我trimToSize这个方法何时调用呢?

trimToSize这个方法在LruCache里面多个方法里面会被调用来检测是否已经满了,比如在往LruCache里面加入一个新的数据的方法put里面,还有在通过get(K
key)这个方法获取一个数据的时候等,都会调用trimToSize来检测一次。

3. 下面看看LruCache的put方法

put方法是向LruCache缓存中添加一条新数据:

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++;
        size += safeSizeOf(key, value); //size加上预put对象的大小
        previous = map.put(key, value);
        if (previous != null) {
//如果之前存在键为key的对象,则size应该减去原来对象的大小
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {//加入重复位置的数据,则移除老的数据
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);//每次新加入对象都需要调用trimToSize检测缓存的数据是否已经满
    return previous;
}

我们看到上面的方法牵扯到线程安全的都加入了synchronized关键字,由此可见LruCache就是线程安全的。

4. 下面看看 LruCache的get方法:

Get方法通过key返回相应的item

我们看一下get的源码:

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++;//取得数据失败次数
    }
/*如果未命中,则试图创建一个对象,这里方法返回null,并没有实现创建对象的方法如果需要事项创建对象的方法可以重写create方法。因为图片缓存时内存缓存没有命中会去<span style="font-family: Arial, Helvetica, sans-serif;">文件缓存中去取或者从网络下载,所以并不需要创建。*/</span>
    V createdValue = create(key);//尝试创建这个数据
    if (createdValue == null) {
        return null;//创建数据失败
    }
 
    synchronized (this) {//加入这个重新创建的数据
        createCount++;//从新创建数据次数
        mapValue = map.put(key, createdValue);
 
        if (mapValue != null) {
   //如果mapValue不为空,则撤销上一步的put操作。
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }
 
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);//检测是否满
        return createdValue;
    }
}

通过key返回相应的item,或者创建返回相应的item。相应的item会移动到队列的头部,如果item的value没有被cache或者不能被创建,则返回null。

我们在上面源码中看到了一个create(key)方法,我们看一下create方法的源码:

protected V create(K key) {
    return null;
}

可以看到源码默认返回了一个null,我们分析上面get的代码,

V createdValue = create(key);//尝试创建这个数据

if (createdValue == null) {

return null;//创建数据失败

}

代码中判断如果create返回空,get也就返回空,表示所查询的不再cache缓存中,在文件缓存中找不到的话就会重新在网络上下载。

那么create一直都会返回null,LruCache中为什么要creat方法呢,我们分析一下如果create不反悔NULL的话会怎么处理,看源码可知他将返回的createdValue添加到map集合中了,然后再将createdValue返回给用户。从上面的分析可以知道,虽然源码中creat方法返回横为NULL,可是我们可以重写create方法来重新创建已经不存在的数据。当然一般情况下不需要这样做,当查找不到相应缓存时会重新从网络上下载。

5. 最后再看看remove方法:

/**
 * 删除key相应的cache项,返回相应的value
*/
    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }  

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }  

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

        return previous;
    } 

Remove方法是删除key相应的cache项,返回相应的value。所以我们可以主动移除缓存中所缓存的数据。

介绍到这里,LruCache的主要代码就介绍完毕了,

下面分享一个用于缓存下载的网络图片的cache实现:

public class BitmapCache implements ImageCache {
	private static LruCache<String, Bitmap> mCache;
	public BitmapCache() {
		if (mCache == null) {
			// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
		    // LruCache通过构造函数传入缓存值,以KB为单位。
		    int maxMemory = (int) (Runtime.getRuntime().maxMemory());
		    // 使用最大可用内存值的1/8作为缓存的大小。
		    int cacheSize = maxMemory / 10;
			mCache = new LruCache<String, Bitmap>(cacheSize) {
				@Override
				protected int sizeOf(String key, Bitmap bitmap) {
					return bitmap.getRowBytes() * bitmap.getHeight();
				}
			};
		}
	}
	@Override
	public Bitmap getBitmap(String url) {
		return mCache.get(url);
	}
	@Override
	public void putBitmap(String url, Bitmap bitmap) {
		mCache.put(url, bitmap);
	}
}

在应用中就可以通过getBitmap、putBitmap来获取或添加图片数据了。当调用getBitmap返回的是null的话,我们就需要重新在网络下载。

时间: 2024-10-12 21:11:37

LruCache源码浅析的相关文章

Android网络通信Volley框架源码浅析(三)

尊重原创 http://write.blog.csdn.net/postedit/26002961 通过前面浅析(一)和浅析(二)的分析,相信大家对于Volley有了初步的认识,但是如果想更深入的理解,还需要靠大家多多看源码. 这篇文章中我们主要来研究一下使用Volley框架请求大量图片的原理,在Android的应用中,通过http请求获取的数据主要有三类: 1.json 2.xml 3.Image 其中json和xml的获取其实原理很简单,使用Volley获取感觉有点大财小用了,了解Volle

Volley框架源码浅析(一)

尊重原创http://blog.csdn.net/yuanzeyao/article/details/25837897 从今天开始,我打算为大家呈现关于Volley框架的源码分析的文章,Volley框架是Google在2013年发布的,主要用于实现频繁而且粒度比较细小的Http请求,在此之前Android中进行Http请求通常是使用HttpUrlConnection和HttpClient进行,但是使用起来非常麻烦,而且效率比较地下,我想谷歌正式基于此种原因发布了Volley框架,其实出了Voll

PM2源码浅析

PM2工作原理 最近在玩一个游戏,<地平线:黎明时分>,最终Boss是一名叫黑底斯的人,所谓为人,也许不对,黑底斯是一段强大的毁灭进程,破坏了盖娅主进程,从而引发的整个大陆机械兽劣化故事. 为什么要讲这么一段呢,是希望大家可以更好地理解pm2的原理,要理解pm2就要理解god和santan的关系,god和santan的关系就相当于盖娅和黑底斯在pm2中的01世界中,每一行代码每一个字节都安静的工作god就是Daemon进程 守护进程,重启进程,守护node程序世界的安宁,santan就是进程的

Android源码浅析(一)——VMware Workstation Pro和Ubuntu Kylin 16.04 LTS安装配置

Android源码浅析(一)--VMware Workstation Pro和Ubuntu Kylin 16.04 LTS安装配置 最近地方工作,就是接触源码的东西了,所以好东西还是要分享,系列开了这么多,完结 的也没几个,主要还是自己覆盖的太广了,却又不精通,嘿嘿,工作需要,所以写下了本篇博客 一.VMware 12 我选择的虚拟机试VMware,挺好用的感觉,下载VMware就不说了,善用搜索键嘛,这里我提供一个我现在在用的 下载地址:链接:http://pan.baidu.com/s/1k

ReactiveCocoa2 源码浅析

ReactiveCocoa2 源码浅析 标签(空格分隔): ReactiveCocoa iOS Objective-C ? 开车不需要知道离合器是怎么工作的,但如果知道离合器原理,那么车子可以开得更平稳. ReactiveCocoa 是一个重型的 FRP 框架,内容十分丰富,它使用了大量内建的 block,这使得其有强大的功能的同时,内部源码也比较复杂.本文研究的版本是2.4.4,小版本间的差别不是太大,无需担心此问题. 这里只探究其核心 RACSignal 源码及其相关部分.本文不会详细解释里

【Spark Core】任务执行机制和Task源码浅析2

引言 上一小节<任务执行机制和Task源码浅析1>介绍了Executor的注册过程. 这一小节,我将从Executor端,就接收LaunchTask消息之后Executor的执行任务过程进行介绍. 1. Executor的launchTasks函数 DriverActor提交任务,发送LaunchTask指令给CoarseGrainedExecutorBackend,接收到指令之后,让它内部的executor来发起任务,即调用空闲的executor的launchTask函数. 下面是Coars

Volley框架源码浅析(二)

尊重原创 http://write.blog.csdn.net/postedit/25921795 在前面的一片文章Volley框架浅析(一)中我们知道在RequestQueue这个类中,有两个队列:本地队列和网络队列 /** The cache triage queue. */ private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<

Android手势源码浅析-----手势绘制(GestureOverlayView)

Android手势源码浅析-----手势绘制(GestureOverlayView)

【Spark】Stage生成和Stage源码浅析

引入 上一篇文章<DAGScheduler源码浅析>中,介绍了handleJobSubmitted函数,它作为生成finalStage的重要函数存在,这一篇文章中,我将就DAGScheduler生成Stage过程继续学习,同时介绍Stage的相关源码. Stage生成 Stage的调度是由DAGScheduler完成的.由RDD的有向无环图DAG切分出了Stage的有向无环图DAG.Stage的DAG通过最后执行的Stage为根进行广度优先遍历,遍历到最开始执行的Stage执行,如果提交的St