Android源码解析之LruCache(五)
前言
在实际开发中,我们经常要加载很多大图,大多数情况下都是从服务器中下载下来,然后显示到图片的控件中。这里就会存在一个问题,如果每次加载图片都要从网络上下载,这样就会导致效率非常慢,影响用户的体验。所以我们会想到可以把图片缓存到内存中,第二次之后加载图片就从内存中加载,这样效率就会很快。但是我们要知道,如果我们把大量的图片都存到内存中,当内存不足的时候,就会导致OOM。所以这样一来LruCache就是为了解决这个问题而出现的。
一、LruCache的简单使用
LruCache用中文翻译就是最近最少使用算法,它会将图片缓存到内存中。具体原理我们先不说,我们先介绍LruCache的简单使用方法,然后再从源码的角度分析LruCache的实现原理。
/**
* 自定义图片加载类
*/
public class MyImageLoader {
private static final String TAG ="MyImageLoader" ;
/**
* LruCache 内存缓存类
*/
private LruCache<String, Bitmap> lruCache;
private Context context;
public MyImageLoader(Context context) {
this.context = context;
//获取最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//使用最大内存的1/8作为缓存大小
int cacheSize = maxMemory / 8;
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
//每次存入缓存的时候调用该方法
//存入的图片的大小
return super.sizeOf(key, value);
}
};
}
/**
* 显示图片
*
* @param imageView 显示控件
* @param url url 作为key
*/
public void showImage(ImageView imageView, String url) {
//从缓存中获取bitmap
Bitmap bitmap = lruCache.get(url);
//判断缓存中的bitmap是否为空
if (bitmap == null) {
//如果为空,则去下载该图片
downLoad(imageView, url);
Log.e(TAG, "showImage: 从网上下载的图片" );
} else {
//如果不为空,则直接展示缓存中的bitmap
imageView.setImageBitmap(bitmap);
Log.e(TAG, "showImage: 从缓存中加载图片" );
}
}
/**
* 下载图片
*
* @param imageView
* @param url
*/
private void downLoad(final ImageView imageView, final String url) {
AsyncTask<String, Integer, Bitmap> asyncTask = new AsyncTask<String, Integer, Bitmap>() {
@Override
protected Bitmap doInBackground(String... integers) {
//在这里执行异步任务,下载图片。在这里就模拟从网络下载图片
//从项目中加载一张图片,模拟从网络下载
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
return bitmap;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//显示图片
imageView.setImageBitmap(bitmap);
//并把图片放入缓存中
lruCache.put(url, bitmap);
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
};
asyncTask.execute(url);
}
}
我们自定义一个加载图片的类MyImageLoader ,这个类很简单
- 首先在构造方法中初始化缓存的大小,一般为最大可用内存的8分之1
- 然后创建LruCache对象,两个参数分别为key 和 value,也就是说缓存中以键值对的显示存在。当我们取缓存中的值的时候,可用通过键来取。
- showImage方法用来加载图片,传入图片控件ImageView 以及要下载图片的url参数,并用url做完缓存的键。首先从缓存中读取bitmap,然后判断缓存中的bitmap是否为空,如果不为空直接显示图片。如果为空调用downLoad方法去下载图片,在downLoad方法中,我们就不去网络上下载了,直接从项目中加载一张图片用来模拟下载。下载成功后我们加载图片,并把图片通过url为键存入到缓存中。
//并把图片放入缓存中
lruCache.put(url, bitmap);
接下来我们看下具体的使用:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ImageView ivEtxt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ivEtxt = (ImageView) findViewById(R.id.iv_etxt);
final MyImageLoader imageLoader = new MyImageLoader(this);
ivEtxt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
imageLoader.showImage(ivEtxt, "http://baidu.com");
}
});
}
}
我们点击ImageView事件,加载图片,点击两次得到的打印结果如下:
08-23 14:24:26.377 3071-3071/com.mujin.keji.myapplication E/MyImageLoader: showImage: 从网上下载的图片
08-23 14:24:27.144 3071-3071/com.mujin.keji.myapplication E/MyImageLoader: showImage: 从缓存中加载图片
我们看到第一次是从网下下载的图片,第二次是从缓存中加载的图片,并成功的显示到了ImageViw中。
image.png
以上我们就介绍了LruCache的简单使用,具体详细的用法就不具体介绍了。下面将从源码的角度分析LruCache的原理。
二、从源码的角度分析LruCache的实现原理
首先我们看下LruCache的构造方法。
/**
* @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);
}
在LruCache的构造方法中传入了maxSize参数,并把maxSize设置到成员变量,也就是我们设定的缓存的大小。接着创建了一个LinkedHashMap对象。在创建LinkedHashMap对象的时候就是最重要的核心内容。 new LinkedHashMap<K, V>(0, 0.75f, true);分别传入三个参数,第一个参数0表示初始化长度为0、第二个参数0.75表示已0.75作为因子,当容量达到总容量的百分之75的时候,会把内存增加一半。第三个参数最为重要,true表示以访问顺序排序,就是当我们对LinkedHashMap的某个元素进行put或者get操作的时候,会把当前元素放到最后,重新排序。所以每次我们get或者put的时候,其实就是把元素放到了最后,所以最少访问的数据自然跑到了最前面。
接下来我们看put方法,把数据放入缓存中。
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
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) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
在put方法中首先对size自增,就是在当前缓存中存在的元素的大小中再增加当前元素的大小,然后再返回当前缓存中元素的大小。
size += safeSizeOf(key, value);
然后把数据以键值对的形式存到map中
previous = map.put(key, value);
在这里要注意的是,如果mao中已经存在了该元素,则返回previous ,不会重复插入。所以当previous 不为空的时候,size 需要自减。
接着如果previous 不为空,调用了entryRemoved方法,该方法是一个空方法,我们如果需要了解具体的数据信息,可以重写该方法。
if (previous != null) {
entryRemoved(false, key, previous, value);
}
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
最后调用trimToSize方法
trimToSize(maxSize);
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
在这个方法中其实是一个while循环的操作,我们看下跳出循环的条件。
if (size <= maxSize || map.isEmpty()) {
break;
}
也就是说,如果缓存中元素的大小还没有超过我们设定的缓存大小,或者map链表中还没有值,就跳出循环,不做任何操作。但是如果说,缓存中的元素的大小已经达到了我们设定的缓存的大小,那么就需要把链表中的某个元素给去掉,具体去掉哪一个呢,我们看下面的代码。
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
我们看到移除的是map中的第一个元素,为什么移除第一个元素呢,我们在上面分析的时候知道,在创建map的时候调用了 new LinkedHashMap<K, V>(0, 0.75f, true);方法,传入了true这个参数,每次被访问的元素都会放在最后然后重新进行排序,那么最少访问次数的元素自然而然就在链表的头部了。所以移除掉第一个元素,也就是达到了最近最少使用算法这个效果。也就是经常不访问的元素就被移除掉了。
下面我们在看下get方法,从缓存中读取元素。
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
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.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last 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;
}
}
首先从map中根据key获取元素,如果元素不为空,说明缓存中有值,返回当前元素。
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.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last 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;
}
如果为空的话,我们调用 V createdValue = create(key);方法,create方法是一个空方法,我们可以手动实现,也就是说,如果为空的话,我们可以进行手动实现,给缓存中添加指定的元素。然后调用trimToSize方法,一样的原理,根据缓存中的大小和设定的大小比较看是否要移除第一个元素。
以上我们就对LruCache的源码做了简单的分析。接下来我们做一个简单的小结。
1、在LruCache中维护了一个LinkedHashMap链表,并且该链表以访问顺序进行排序,每次访问的元素会放在最后。
2、调用put方法给LinkedHashMap中添加元素,并且每插入一个元素,就要计算当前链表中的元素大小和设定的缓存的大小进行比较,如果达到了设定缓存的大小,把链表的第一个元素移除,也就是很少访问的元素移除掉。
3、调用get方法,获取LinkedHashMap中的元素。如果元素为空,我们还可以重写create方法,手动添加特定的元素到LinkedHashMap中。