为了减少我们从网络获取数据(图片)的次数,我们会从网络获取到之后,缓存到我们的内存中。所以在我们网络编程中,缓存技术显得非常重要。
-
使用软引用或者弱引用
使用软引用保存数据是Android 2.3之前很常用的一种缓存技术。现在已经不推荐这种方式缓存数据了。虽然不推荐使用,但我们稍微了解一下。
代码如下:
private Map<String, SoftReference<Bitmap>> imageMap = new HashMap<String, SoftReference<Bitmap>>()
public Bitmap loadBitmap(final String imageUrl,final ImageCallBack imageCallBack) {
SoftReference<Bitmap> reference = imageMap.get(imageUrl);
if(reference != null) {
if(reference.get() != null) {
return reference.get();
}
}
// 这里仅仅做一个例子,随便写的
Bitmap bitmap = (Bitmap) getFromNetWork();
// 加入到缓存
imageMap.put(imageUrl, new SoftReference<Bitmap>(bitmap));
return bitmap;
}
不推荐使用的原因是:
(1)Android2.3之后,垃圾回收器会更倾向于回收持有软引用或弱引用的对象, 这让软引用和弱引用变得不再可靠。
(2)另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
so 我们讲一下更好的缓存技术:LruCache -
LruCache缓存
简单说一下原理:LruCache缓存数据是采用持有数据的强引用来保存一定数量的数据的.每次用到(获取)一个数据时,这个数据就会被移动(一个保存数据的)队列的头部,当往这个缓存里面加入一个新的数据时,如果这个缓存已经满了,就会自动删除这个缓存队列里面最后一个数据,这样一来使得这个删除的数据没有强引用而能够被gc回收。
下面我们从源码上解读:
先看看构造函数:
public LruCache(int maxSize) {//指定缓存数据的数量
if (maxSize <= 0) {//必须大于0
throw new IllegalArgumentException(maxSize <= 0);
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<k, v="">(0, 0.75f, true);//创建一个LinkedHashMap,并且按照访问数据的顺序排序
}
从代码上看,我们知道LruCache是使用LinkedHashMap去储存数据的,这样可以维护这些数据相应的顺序的。
那我们缓存数据的数量怎么算?是个数?还是大小?我们发现有一个方法:
protected int sizeOf(K key, V value) {
//子类覆盖这个方法来计算出自己的缓存对于每一个保存的数据所占用的量
return 1;//默认返回1,这说明:默认情况下缓存的数量就是指缓存数据的总个数(每一个数据都是1).
}
从这里我们看出,我们需要重写这个方法,达到我们所需要的缓存数量。
那如果我使用LruCache来保存bitmap的图片,并且希望缓存的容量是4M那这么做?在原文的说明中,android给来这样一个实例:
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<string, bitmap=""> bitmapCache = new LruCache<string, bitmap="">(cacheSize) {
//保存bitmap的LruCache,容量是4M
protected int sizeOf(String key, Bitmap value) {
//计算每一个缓存的图片所占用内存大小
return value.getByteCount();
}
};
这样我们就可以指定最大缓存是4M,我们要缓存的bitmap的大小,这样就可以计算了。
那么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这个方法在LruCache里面多个方法里面会被调用来检测是否已经满了,比如在往LruCache里面加入一个新的数据的方法put里面,还有在通过get(K key)这个方法获取一个数据的时候等,都会调用trimToSize来检测一次。接下来看看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) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {//加入重复位置的数据,则移除老的数据
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);//检测缓存的数据是否已经满
return previous;
}
到这里你会发现,原来对 LruCache的操作都加了synchronized来保证线程安全。
如果LruCache中已经删除了一个数据,可是现在又调用LruCache的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++;//取得数据失败次数
}
/*
* 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;
}
}
从上面的分析可以知道,我们可以从写create方法来重新创建已经不存在的数据.这个方法默认情况是什么也不做的,所以需要你自己做
protected V create(K key) {
return null;
}
那我们怎么使用这个类呢?
//获取系统分配给每个应用程序的最大内存,每个应用系统分配32M
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int mCacheSize = maxMemory / 8;
//给LruCache分配1/8 4M
mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){
//必须重写此方法,来测量Bitmap的大小
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
/*
* 添加Bitmap到内存缓存
/
mMemoryCache.put(key, bitmap);
/*
* 从内存缓存中获取一个Bitmap
/
mMemoryCache.get(key);
/*
* 移除缓存
*/
Bitmap bm = mMemoryCache.remove(key);
if (bm != null) {
bm.recycle();
}