LruCache
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作(garbage collection:垃圾回收)。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,开发者会经常使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
强引用:
平时我们编程的时候例如:Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,
那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OOM异常,
使程序异常终止,也不会回收具有强引用的对象来解决内存不足问题。
软引用:
软引用类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的
内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引
用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联
的引用队列中。
使用软引用能防止内存泄露,增强程序的健壮性。
弱引用:
gc的时候,会被回收。
虚引用:
最优先回收
强引用缓存图片:
//强引用:容易造成内存溢出,因为不会被回收,该对象的引用一直都在
private HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap>();
/**
* 写内存缓存
*
* @param url
* @param bitmap
*/
public void setCache(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}
/**
* 读内存缓存
*
* @param url
*/
public Bitmap getCache(String url) {
return mMemoryCache.get(url);
}
软引用缓存图片:
//软应用:使用软引用:内存不足时,会被回收。
private HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<>();
/**
* 写内存缓存
*
* @param url
* @param bitmap
*/
public void setCache(String url, Bitmap bitmap) {
//使用软引用将bitmap包装起来
SoftReference<Bitmap> soft = new SoftReference<Bitmap>(bitmap);
mMemoryCache.put(url, soft);
}
/**
* 读内存缓存
*
* @param url
*/
public Bitmap getCache(String url) {
SoftReference<Bitmap> softReference = mMemoryCache.get(url);
if (softReference != null) {
Bitmap bitmap = softReference.get();
return bitmap;
}
return null;
}
LruCache缓存图片:
-
从 Android 2.3 (API Level9)开始,垃圾回收器会更倾向于回收(内存足够时也会回收)持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。Google建议使用LruCache。
-
LruCache:内存缓存技术,这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用 存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
-
Lru: least recentlly used 最近最少使用算法 自己可以控制内存的大小,可以将最近最少使用的对象回收掉, 从而保证内存不会超出范围
//LruCache;
private LruCache<String, Bitmap> mMemoryCache;
public MemoryCacheUtil() {
// 获取分配给app的内存大小
long maxMemory = Runtime.getRuntime().maxMemory();
// 使用最大可用内存值的1/8作为缓存的大小。
mMemoryCache = new LruCache<String, Bitmap>((int) (maxMemory / 8)) {
//返回每个对象的大小
@Override
protected int sizeOf(String key, Bitmap value) {
//int byteCount = value.getRowBytes() * value.getHeight();// 计算图片大小:每行字节数*高度
int byteCount = value.getByteCount();
return byteCount;
}
};
}
/**
* 写内存缓存
*
* @param url
* @param bitmap
*/
public void setCache(String url, Bitmap bitmap) {
mMemoryCache.put(url, bitmap);
}
/**
* 读内存缓存
*
* @param url
*/
public Bitmap getCache(String url) {
return mMemoryCache.get(url);
}
图片的三级缓存案例
图片的三级缓存:
- 先从内存缓存中查找图片,如果找到,直接加载;
- 本地缓存;
- 网络缓存。
网络缓存:
package com.xiaoyehai.threadpool.util;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.widget.ImageView;
/**
* 网络缓存工具类
* Created by xiaoyehai on 2016/11/22.
*/
public class NetCacheUtils {
private MemoryCacheUtil mMemoryCacheUtil;
private LocalCacheUtils mLocalCacheUtils;
public NetCacheUtils(MemoryCacheUtil mMemoryCacheUtil, LocalCacheUtils mLocalCacheUtils) {
this.mMemoryCacheUtil = mMemoryCacheUtil;
this.mLocalCacheUtils = mLocalCacheUtils;
}
public void getBitmapFromNet(ImageView imageView, String url) {
new BitmapTask().execute(imageView, url); //启动AsyncTask
imageView.setTag(url); //设置标记,处理错位
}
/**
* AsyncTask:异步任务类,可以实现异步请求和主界面更新(Handler+线程池)
* 1.doInBackground里面的参数类型
* 2.onProgressUpdate里面的参数类型
* 3.onPostExecute里面的参数类型及doInBackground的返回类型
*/
class BitmapTask extends AsyncTask<Object, Integer, Bitmap> {
private ImageView iv;
private String url;
@Override
protected void onPreExecute() {
// TODO: 2017/1/31 预加载,运行在主线程
super.onPreExecute();
}
@Override
protected Bitmap doInBackground(Object... params) {
// TODO: 2017/1/31 正在加载,运行在子线程,可以直接异步请求
iv = (ImageView) params[0];
url = (String) params[1];
//开始下载图片
Bitmap bitmap = HttpUtils.loadBitmap(url);
return bitmap;
}
@Override
protected void onProgressUpdate(Integer... values) {
// TODO: 2017/1/31 更新进度的方法,运行在主线程
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// TODO: 2017/1/31 加载结束,运行在主线程,可以直接更新ui
String str_url = (String) iv.getTag();
if (bitmap != null && str_url.equals(url)) {
iv.setImageBitmap(bitmap);
//缓存到本地
mLocalCacheUtils.setCache(url, bitmap);
//缓存到内存
mMemoryCacheUtil.setCache(url, bitmap);
}
super.onPostExecute(bitmap);
}
}
}
本地缓存:
package com.xiaoyehai.threadpool.util;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* 本地(sd卡)缓存工具类
* Created by xiaoyehai on 2016/11/22.
*/
public class LocalCacheUtils {
//缓存的文件夹路径
public static final String FILE_PATH = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/image_cache";
/**
* 写本地缓存
*
* @param url
* @param bitmap
*/
public void setCache(String url, Bitmap bitmap) {
File dir = new File(FILE_PATH);
if (!dir.exists() || dir.isDirectory()) {
dir.mkdirs();//创建文件夹
}
try {
String fileName = MD5Encoder.encode(url);
File cacheFile = new File(dir, fileName);
// 参1:图片格式;参2:压缩比例0-100; 参3:输出流
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(cacheFile));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 读本地缓存
*
* @param url
* @return
*/
public Bitmap getCache(String url) {
try {
File cacheFile = new File(FILE_PATH, MD5Encoder.encode(url));
if (cacheFile.exists()) {
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(cacheFile));
return bitmap;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class MD5Encoder {
public static String encode(String string) throws Exception {
byte[] hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
if ((b & 0xFF) < 0x10) {
hex.append("0");
}
hex.append(Integer.toHexString(b & 0xFF));
}
return hex.toString();
}
}
内存缓存:
package com.xiaoyehai.threadpool.util;
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
/**
* 内存缓存
* 从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收(内存足够时也会回收)持有软引用或弱引用的对象,
* 这让软引用和弱引用变得不再可靠。Google建议使用LruCache。
* <p>
* Psrson p=new Person;强引用:不会被回收
* 栈:存放对象的引用(p)
* 堆:存放对象(new Person)
* <p>
* 垃圾回收器:只回收没有引用的对象,不及时
* 强引用(默认):垃圾回收器不会回收
* 软引用:内存不足时,会被回收。使用软引用能防止内存泄露,增强程序的健壮性。
* 弱引用:内存不足时,会被回收。
* 虚引用:最优先回收
* <p>
* LruCache:内存缓存技术,这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用
* 存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
* Lru: least recentlly used 最近最少使用算法
* 自己可以控制内存的大小,可以将最近最少使用的对象回收掉, 从而保证内存不会超出范围
* <p>
* Created by xiaoyehai on 2016/11/22.
*/
public class MemoryCacheUtil {
//强引用:容易造成内存溢出,因为不会被回收,该对象的引用一直都在
//private HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap>();
//软应用:使用软引用:内存不足时,会被回收。
//private HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<>();
//LruCache;
private LruCache<String, Bitmap> mMemoryCache;
public MemoryCacheUtil() {
// 获取分配给app的内存大小
long maxMemory = Runtime.getRuntime().maxMemory();
// 使用最大可用内存值的1/8作为缓存的大小。
mMemoryCache = new LruCache<String, Bitmap>((int) (maxMemory / 8)) {
//返回每个对象的大小
@Override
protected int sizeOf(String key, Bitmap value) {
//int byteCount = value.getRowBytes() * value.getHeight();// 计算图片大小:每行字节数*高度
int byteCount = value.getByteCount();
return byteCount;
}
};
}
/**
* 写内存缓存
*
* @param url
* @param bitmap
*/
public void setCache(String url, Bitmap bitmap) {
//mMemoryCache.put(url, bitmap);
//使用软引用将bitmap包装起来
// SoftReference<Bitmap> soft = new SoftReference<Bitmap>(bitmap);
// mMemoryCache.put(url, soft);
mMemoryCache.put(url, bitmap);
}
/**
* 读内存缓存
*
* @param url
*/
public Bitmap getCache(String url) {
//return mMemoryCache.get(url);
// SoftReference<Bitmap> softReference = mMemoryCache.get(url);
// if (softReference != null) {
// Bitmap bitmap = softReference.get();
// return bitmap;
// }
// return null;
return mMemoryCache.get(url);
}
}
BitmapUtils:
package com.xiaoyehai.threadpool.util;
import android.graphics.Bitmap;
import android.widget.ImageView;
import com.xiaoyehai.threadpool.R;
/**
* 自定义具有三级缓存功能的图片加载工具类
* <p>
* 1.内存缓存:速度最快,不耗流量
* 2.本地sd卡缓存:速度快,不耗流量
* 3.网络缓存:速度慢,浪费流量
* Created by xiaoyehai on 2016/11/22.
*/
public class BitmapUtils {
private MemoryCacheUtil mMemoryCacheUtil;
private LocalCacheUtils mLocalCacheUtils;
private NetCacheUtils mNetCacheUtils;
public BitmapUtils() {
mMemoryCacheUtil = new MemoryCacheUtil();
mLocalCacheUtils = new LocalCacheUtils();
mNetCacheUtils = new NetCacheUtils(mMemoryCacheUtil, mLocalCacheUtils);
}
public void display(ImageView imageView, String url) {
//设置默认图片
imageView.setImageResource(R.mipmap.ic_launcher);
//一级缓存:内存缓存中获取数据
Bitmap bitmap = mMemoryCacheUtil.getCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
//二级缓存:从本地(sd卡)缓存中获取数据
bitmap = mLocalCacheUtils.getCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
//写内存缓存
mMemoryCacheUtil.setCache(url, bitmap);
return;
}
//三级缓存:网络获取数据
mNetCacheUtils.getBitmapFromNet(imageView, url);
}
}
DiskLruCache(磁盘缓存)
LruCache原理
关于Android的三级缓存,其中主要的就是内存缓存和硬盘缓存。
这两种缓存机制的实现都应用到了LruCache算法,今天我们就从使用到源码解析,来彻底理解Android中的缓存机制。
一、Android中的缓存策略
一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的。当缓存满了之后,再想其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。
因此LRU(Least Recently Used)缓存算法便应运而生,LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。
1.LruCache的介绍
LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。
先看下LruCache的 源码
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map; //存放数据的集合
private int size; //当前LruCahce的内存占用大小
private int maxSize; //Lrucache的最大容量
private int putCount; //put的次数
private int createCount; //create的次数
private int evictionCount; //回收的次数
private int hitCount; //命中的次数
private int missCount; //丢失的次数
构造方法:
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);
}
LinkedHashMap的构造方法:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
其中accessOrder设置为true则为访问顺序,为false,则为插入顺序。
以具体例子解释:
当设置为true时:
public static final void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
map.put(6, 6);
map.get(1);
map.get(2);
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
输出结果:
0:0
3:3
4:4
5:5
6:6
1:1
2:2
当设置为false的时候,输出顺序为:
0:0
1:1
2:2
3:3
4:4
5:5
6:6
有以上结果可以看出,这个设置为true的时候,如果对一个元素进行了操作(put、get),就会把那个元素放到集合的最前面,设置为false的时候,无论怎么操作,集合元素的顺序都是按照插入的顺序来进行存储的。
到了这里我们可以知道,这个LinkedHashmap正是实现Lru算法的核心之处,当内容容量达到最大值的时候,只需要移除这个集合的前面的元素直到集合的容量足够存储数据的时候就可以了。
下面我们来看一看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) {
//插入的缓存对象值加1
putCount++;
//增加已有缓存的大小
size += safeSizeOf(key, value);
//向map中加入缓存对象
previous = map.put(key, value);
//如果已有缓存对象,则缓存大小恢复到之前
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//entryRemoved()是个空方法,可以自行实现
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//调整缓存大小(关键方法)
trimToSize(maxSize);
return previous;
}
可以看到put()方法并没有什么难点,重要的就是在添加过缓存对象后,调用 trimToSize()方法,来判断缓存是否已满,如果满了就要删除近期最少使用的算法。
trimToSize()方法
public void trimToSize(int maxSize) {
//死循环
while (true) {
K key;
V value;
synchronized (this) {
//如果map为空并且缓存size不等于0或者缓存size小于0,抛出异常
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
//如果缓存大小size小于最大缓存,或者map为空,不需要再删除缓存对象,跳出循环
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);
}
}
这个方法是一个无限循环,跳出循环的条件是,size < maxSize或者 map 为空。主要的功能是判断当前容量时候已经超出最大的容量,如果超出了maxSize的话,就会循环移除map中的第一个元素,即删除LinkedHashMap中队尾(链表的尾部)的元素,即近期最少访问的,直到达到跳出循环的条件。由上面的分析知道,map中的第一个元素就是最近最少使用的那个元素。
研究完了put方法之后,下面开始研究get方法。
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//获取对应的缓存对象
//get()方法会实现将访问的元素更新到队列头部的功能
mapValue = map.get(key);
if (mapValue != null) {
hitCount++; //命中 + 1
return mapValue;
}
missCount++;//丢失+1
}
V createdValue = create(key); //创建
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++; //创建 + 1
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
//如果有矛盾,意思就是有旧的值,就撤销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;
}
}
当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素**,将会更新队列**,保持整个队列是按照访问顺序排序。这个更新过程就是在LinkedHashMap中的get()方法中完成的。
最后说一下remove方法:
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;
}
由此可见LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队尾元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队头。
LruCache的使用:
int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
int cacheSize = maxMemory/8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
①设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
②重写sizeOf方法,计算出要缓存的每张图片的大小。
注意:缓存的总容量和每个缓存对象的大小所用单位要一致。