昨天讲到了如何使用volley加载网络图片,在MyImageCache中用到了一个LruCache的东西,今天仔细来讲下为什么在MyImageCache用到这个对象。如果没看过上一篇博客,可以先去看下 android中volley框架实现图片加载
如果写过大批量加载图片的应用的人都知道加载大量图片是很容易出现OOM问题,只有内存有1G可用(是当前可用,而不是总内存只有1G)的手机才会不经常出现这样子的问题,但是毕竟这样子的手机是少数,那我们就要去优化加载图片的方法。
LruCache的出现就是去拯救这种问题,当然还有其他方法。它是专门用来处理图片缓存的问题。它有一个特点,当缓存的图片达到了预先设定的值的时候,那么近期使用次数最少(也就是eldest,最年长)的图片就会被回收掉。
在LruCache中只能存储maxSize大小的内容,用size用来记录当前已存储的大小,当超出maxSize的时候,就会去回收那些最少被使用的对象。
LruCache中的部分方法可以重写专门用来存储图片的问题。下面是对源代码的解读,如有错误,请站出来帮妹子我雅正
package com.think.linxuanxuan.volleysample;
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.LinkedHashMap;
import java.util.Map;
/**
* BEGIN LAYOUTLIB CHANGE
* This is a custom version that doesn't use the non standard LinkedHashMap#eldest.
* END LAYOUTLIB CHANGE
*
* A cache that holds strong references to a limited number of values. Each time
* a value is accessed, it is moved to the head of a queue. When a value is
* added to a full cache, the value at the end of that queue is evicted and may
* become eligible for garbage collection.
*
* If your cached values hold resources that need to be explicitly released,
* override {@link #entryRemoved}.
*
*
If a cache miss should be computed on demand for the corresponding keys,
* override {@link #create}. This simplifies the calling code, allowing it to
* assume a value will always be returned, even when there's a cache miss.
*
*
By default, the cache size is measured in the number of entries. Override
* {@link #sizeOf} to size the cache in different units. For example, this cache
* is limited to 4MiB of bitmaps:
*
{@code
* int cacheSize = 4 * 1024 * 1024; // 4MiB
* LruCache bitmapCache = new LruCache(cacheSize) {
* protected int sizeOf(String key, Bitmap value) {
* return value.getByteCount();
* }
* }}
*
* This class is thread-safe. Perform multiple cache operations atomically by
* synchronizing on the cache:
{@code
* synchronized (cache) {
* if (cache.get(key) == null) {
* cache.put(key, value);
* }
* }}
*
* This class does not allow null to be used as a key or value. A return
* value of null from {@link #get}, {@link #put} or {@link #remove} is
* unambiguous: the key was not in the cache.
*
*
This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
* of Android's
* Support Package for earlier releases.
*/
public class LruCache {
//一个LinkedHashMap,用来存储内容,它是非线程安全的,在代码中会看到每次对其读写时都需要synchronized
private final LinkedHashMap map;
/** Size of this cache in units. Not necessarily the number of elements. */
//记录当前map中已经存储的size,这里可以理解为个数或者内存大小,稍后解释
private int size;
//记录用户定义map最大存储的size,这里可以理解为个数或者内存大小,稍后解释
private int maxSize;
private int putCount; //put的个数
private int createCount; //create的个数
private int evictionCount; //eviction(回收)的个数
private int hitCount; //hit的个数
private int missCount; //miss的个数
/**
* @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.
* 构造函数,指定maxSize,当小于0的时候会抛出异常,是runtime异常,因为这种异常不要求catch,大家使用时请注意
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
/**
* Sets the size of the cache.
* @param maxSize The new maximum size.
*
* @hide
* 重新调整map的大小,trimToSize是主要调整map大小的函数
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
/**
* 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.
* get函数,根据key去得到value,如果找不到可以为其创建item,这里大有文章哦!
*/
public final V get(K key) {
//参数检查
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//根据key获取value
mapValue = map.get(key);
//如果找到
if (mapValue != null) {
//找到命中数增加,并返回value
hitCount++;
return mapValue;
}
//如果没找到,miss数增加
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.
* 如果没有找到value的话,可以为key创建相应的item,但是如果有冲突的话,就保持原来的数据
*/
/*
* create函数是根据key来创建相应的item,但是在LruCache中默认返回的是null。
* 因为LruCache未记录被回收的数据,这里读者可以重写该create函数,为key创建相应的item,这里是需要读者自行设计。
* 所以刚刚看源代码的时候觉得很奇怪,明明create函数返回null,下面的代码根本不会执行到,后来才明白其用意。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
//创建数增加
createCount++;
//将刚刚创建的值放入map中,返回的值是在map中与key相对应的旧值(就是在放入new value前的old value)
mapValue = map.put(key, createdValue);
/*
* 当发现有旧值存在时,就撤销刚刚的put操作,做法就是讲old value再put一次。
* 这里读者可能会觉得很奇怪,明明刚刚进入函数时已经去get过一次,为空的时候才会执行到这里,为什么这里还要这么做?
* 那是因为开发人员考虑到在前面create函数中可能会花费一段较长时间,导致在这段时间中已经有其他人put进去,所以再去读了一遍。
* 所以在执行create函数的时候,释放了锁,并没有synchronized。
* 如果这里所有的代码都在synchronized的话,那么就不需要这么做了,因为其他人在这个期间拿不到锁,map一直是一致的。
*/
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
//如果没有旧值的话,为size增加相应的大小
size += safeSizeOf(key, createdValue);
}
}
/*
* 刚刚如果检测到旧值,因为最后旧值还是在map中,但是中途被回收了,所以还是要通知别人这个对象被回收过。
* 所以就调用了entryRemoved,其函数中的参数稍后会解释
*/
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
/*
* 如果刚刚没有检测到旧值,将新值放入map。
* 那么需要重新检测是否size是否超出了maxSize,所以就调用了trimToSize,并返回新值
*/
trimToSize(maxSize);
return createdValue;
}
}
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
* 將key和value放入map中
*/
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
//记录旧值
V previous;
synchronized (this) {
//put数增加
putCount++;
/*
* 为size增加,safeSizeOf返回的是value的size,在LruCache中safeSizeOf返回是1,可以理解为个数。
* 读者可以重写safeSizeOf函数,如果是value是图片的话,其实可以返回bitmap的大小
*/
size += safeSizeOf(key, value);
//将新值放入map中,并得到旧值
previous = map.put(key, value);
/*
* 如果旧值不为空,需要为size减掉旧值的大小。
* 就好像一个篮子中原来放1斤的苹果,后来你替换放入了2斤的苹果,篮子的重量就是1+2-1=2
*/
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//如果旧值不为空,通知旧值对象被回收了
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//重新查看size是否超过了maxSize
trimToSize(maxSize);
return previous;
}
/**
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
* 调整map也就是回收map中的对象,直到size小于maxSize
*/
private void trimToSize(int maxSize) {
//一个循环,每次回收一个value,直到size小于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!");
}
//如果size已经小于maxSize直接返回
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中最少被使用的value,并删除
*/
Map.Entry toEvict = null;
for (Map.Entry 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++;
}
/*
* 每次回收对象就调用该函数,这里参数为true。
* 和其他不一样的原因是因为这里是因为超出maxSize被回收,可以理解为内存不够被删除
*/
entryRemoved(true, key, value, null);
}
}
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code key}.
* 在map中删除key和对应的value
*/
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
//删除key,并返回旧值
previous = map.remove(key);
//如果旧值不为空,则为size减去其旧值大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
*
* The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false
* if the removal was caused by a {@link #put} or {@link #remove}.
* @param newValue the new value for {@code key}, if it exists. If non-null,
* this removal was caused by a {@link #put}. Otherwise it was caused by
* an eviction or a {@link #remove}.
* 读者可以重写该函数,以便你知道该对象什么时候被回收。
* 第一个参数evicted为true时,说明内存不够回收(size超出maxSize),为false时说明是在put和get或者remove的时候人为删掉
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
*
*
The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
*
If a value for {@code key} exists in the cache when this method
* returns, the created value will be released with {@link #entryRemoved}
* and discarded. This can occur when multiple threads request the same key
* at the same time (causing multiple values to be created), or when one
* thread calls {@link #put} while another is creating a value for the same
* key.
* 读者可以重写该create函数,根据key返回相应的value,该函数在上面已经提及,不赘述
*/
protected V create(K key) {
return null;
}
/**
* 返回该value的大小,这里默认返回1,可以理解为个数,当然你可以重写该函数返回其他的,根据你的需求来定
* 例如value为bitmap的时候,可以返回bitmap的大小
* return bitmap.getRowBytes() * bitmap.getHeight() / 1024
* @param key
* @param value
* @return
*/
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
*
*
An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
return 1;
}
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
* 清除map中全部的数据
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return size;
}
/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
public synchronized final int maxSize() {
return maxSize;
}
/**
* Returns the number of times {@link #get} returned a value that was
* already present in the cache.
*/
public synchronized final int hitCount() {
return hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return missCount;
}
/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
public synchronized final int createCount() {
return createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return evictionCount;
}
/**
* Returns a copy of the current contents of the cache, ordered from least
* recently accessed to most recently accessed.
*/
public synchronized final Map snapshot() {
return new LinkedHashMap(map);
}
@Override public synchronized final String toString() {
int accesses = hitCount + missCount;
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
maxSize, hitCount, missCount, hitPercent);
}
}
这是LruCache提供的可以重写的方法
看到这里大家应该都懂了吧。刚刚在写了个小代码,测试了下,果然有用。
代码的地址
git clone https://github.com/LxxCaroline/VolleySample.git
沿用了Volley显示照片的代码,主要修改的代码在MyImageCache中对LruCache的初始化和部分函数的重写
cache = new LruCache(200 * 1024) {
// cache = new LruCache(5 * 1024) {
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
Log.d("tag", "remove==============key:" + key + " is removed. evicted:" + evicted);
}
@Override
protected int sizeOf(String key, Bitmap value) {
Log.d("tag", "size of-----------value size is " + value.getRowBytes() * value.getHeight() /
1024 + "--------key:" + key);
return value.getRowBytes() * value.getHeight() / 1024;
}
};
当把cache的maxSize初始化为200*1024,因为空间很大,list的size设为65,通过打印的log中看出没有因为内存不够remove图片,这个时候我观察了内存,大概消耗了100M。
当把cache的maxSize初始化为5*1024,因为空间不足,从log中可以看出经常remove掉一些图片,这个时候内存消耗不大,大概只消耗了44M,所以还是较好的改善了内存管理的问题。
这里推荐一个比较好的博客,是讲图片加载的,可以看看 Android高效加载大图、多图解决方案,有效避免程序OOM