Android的缓存方法有内存缓存和磁盘缓存,像一些实时的图片展示一般都是用内存缓存的,例如微博、朋友圈的图片;而一些相对固定的图片数据比如有些客户端的图片资源需要从网上下载然后存到磁盘中,以后每次启动只需从缓存中获取即可。今天就来探究一下内存缓存的实现及其原理。
Android的内存缓存使用LruCache类实现,如果容器已满,就使用LRU置换算法替换掉某个数据。什么是LRU置换算法呢?
LRU(Least Recently Used)中文为最近最久未使用算法,当容器满时选择最近最久没被使用的数据予以淘汰。该算法赋予每个数据一个访问字段,用来记录一个数据自上次被访问以来所经历的时间t,当须淘汰一个数据时,选择现有数据中其t值最大的,即最近最久未使用的数据予以淘汰。这种算法在系统的内存页面置换也有用到。
先来看看v4包中的LruCache这个类
private final LinkedHashMap map;
/**
* @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(0, 0.75f, true);
}
可以看到,LruCache内部是使用一个LinkedHashMap来保存键值对,达到缓存的目的。并且构造方法上的注释写得很清楚,分析HashMap的源码也可知,默认情况下maxSize是保存的键值对的个数,每次put键值对进来的时候会调用trimToSize(maxSize)来判断是否超出了最大值,如果超出了则移除最近最久未被使用的元素。
看看put方法:
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);
previous = map.put(key, value);
if (previous != null) { //key在之前已存在
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value); //空方法,需要则重写
}
trimToSize(maxSize); //判断size是否超过maxSize
return previous;
}
添加键值对后让当前size加上计算得到新添加的大小,safeSizeOf()方法默认是返回1的,map.put的返回值不为空,则传入的key在之前已存在,覆盖掉旧值,将旧值返回,所以size减掉移除的value。最后调用trimToSize()方法判断当前大小size是否超出了maxSize,超出了则移除最近最久未访问的元素。
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; //需要时重写该方法
}
所以当我们需要以存储容量来设置缓存的最大值时,必须要重写sizeOf()方法,让其返回计算所得value占用的存储空间的值,并且在LruCache的构造参数传入存储最大值,如new LruCache(5 * 1024 * 1024)。(以bit为单位,当然单位是自己随便设的)
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 || map.isEmpty()) {
break;
}
Map.Entry toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
trimToSize()方法直接进入一个无限循环,如果size超过maxSize则移除元素,直到不超过maxSize,entryRemoved()是个空方法,有需要可重写。
LruCache类可以说非常地简单,真正复杂的操作都在HashMap中,下面来看一个假装新闻demo。
新闻列表页:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_cache);
lv = (ListView) findViewById(R.id.lv_activity_cache);
Title title = new Title();
title.setTitle("新闻一");
title.setBmpUrl("http://119.29.55.18/app_logo.png");
mDatas.add(title);
adapter = new MyAdapter(this);
lv.setAdapter(adapter);
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView> parent, View view, int position, long id) {
Intent intent = new Intent(CacheActivity.this,NewsActivity.class);
intent.putExtra("Title",mDatas.get(position).getTitle());
intent.putExtra("BmpUrl",mDatas.get(position).getBmpUrl());
startActivity(intent);
}
});
}
使用单例模式封装LruCache:
public class ImageCache extends LruCache{
private static ImageCache sImageCache;
public ImageCache(int maxSize) {
super(maxSize);
}
public static ImageCache getInstance(){
if(sImageCache == null){
sImageCache = new ImageCache(2);
}
return sImageCache;
}
}
新闻详情页:
public class NewsActivity extends Activity {
private ImageCache mImageCache;
private TextView tv;
private ImageView iv;
private String title;
private String bmpUrl;
private Bitmap bmp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_news);
Intent intent = getIntent();
title = intent.getStringExtra("Title");
bmpUrl = intent.getStringExtra("BmpUrl");
tv = (TextView) findViewById(R.id.tv_news_content);
tv.setText(title);
iv = (ImageView) findViewById(R.id.iv_news_image);
mImageCache = ImageCache.getInstance();
bmp = mImageCache.get(bmpUrl);
if(bmp == null){
new LoadImageTask().execute(bmpUrl);
}else{
iv.setImageBitmap(bmp);
Toast.makeText(this,"从缓存中获取!!!",Toast.LENGTH_SHORT).show();
}
}
public Bitmap getImage(String u){
try {
URL url = new URL(u);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setUseCaches(false);
InputStream is = connection.getInputStream();
Bitmap bmp = BitmapFactory.decodeStream(is);
is.close();
return bmp;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public class LoadImageTask extends AsyncTask{
@Override
protected Bitmap doInBackground(String... params) {
bmp = getImage(params[0]);
mImageCache.put(params[0],bmp);
return bmp;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
iv.setImageBitmap(bmp);
Toast.makeText(NewsActivity.this,"从网上下载!!!",Toast.LENGTH_SHORT).show();
}
}
值得一提的是还专门去试验过替换算法,比如这里设置maxSize为2,先存键值对1,再存键值对2,再存键值对3,会发现键值对1被移除了;而先存键值对1,再存键值对2,再访问(get)键值对1的值,再存键值对3,会发现键值对2被移除了,符合LRU。
磁盘缓存应该就是入库或者文件保存,下次探究磁盘缓存相关的知识~