日常我们写程序的时候经常会使用到网络的图片,如果我们每次都去网上加载,那么性能难免会差一些,并且网络情况并不是总是 那么好,那么这时候我们就需要使用缓存了,我们学习android都知道图片的三级缓存,分别是内存缓存,硬盘缓存,网络缓存。
它的大体流程是这样的,给定一个网址,加载一张图片
- 如果内存缓存中存在,那就取出来,放上去,如果没有就找硬盘缓存
- 如果硬盘缓存中存在,那就取出来,放上去,并添加到内存缓存中,如果没有就请求网络
- 请求网络,把图片放上去,存一份到内存缓存和硬盘缓存中
接下来我们来分析一下到底是怎么实现的
一、LruCache
LruCache使用的是LRU算法,也叫最近最少使用算法,就是不断往里面存东西,超过上限,把最近最少的对象先淘汰,DiskCache使用的也是该算法。
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对象,和HashMap一样,因为里面就是用LinkHashMap实现的(之后会讲),需要指定键值对类型,传入缓存可用空间大小,并实现sizeof方法,对存入的对象的大小进行计算。
源码分析
1.构造函数
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);
}
实例化的时候传入的可用空间大小在构造函数中被赋值给成员变量,并示例化了一个LinkHashMap,起始容量为0,负载因子为0.75,并将accessOrder设置为了true
public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
LinkHashMap默认是插入顺序的,当把accessOrder设置为true的时候就变成了访问顺序。
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
当LinkHashMap调用get方法时会调用recordAccess方法
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
该方法会将该元素删除并添加到队列头部
2.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方法主要做了三件事,第一计算当前已用空间,第二讲对象存入,第三调整缓存
3.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);
}
}
在该方法中会不断取队尾的元素进行移除操作,直到当前缓存大小小于最大缓存空间大小。
4.get方法
public final V get(K key) {
//key为空抛出异常
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//获取对应的缓存对象
//get()方法会实现将访问的元素更新到队列头部的功能
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
get方法很简单,只是调用LinkHashMap的get方法
二、DiskCache
DIskLruCache类并不在Android SDK中,它并不是由Google官方编写的,但官方对其进行了推荐
要想使用该类需要进行下载,点击下载
或者使用Maven下载
<dependency>
<groupId>com.jakewharton</groupId>
<artifactId>disklrucache</artifactId>
<version>2.0.2</version>
</dependency>
或者gradle
compile 'com.jakewharton:disklrucache:2.0.2'
1.创建硬盘缓存对象
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
该方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。
对于不同版本的程序,硬盘缓存会清空之前的存储,存储新的缓存
缓存的地址可以使用context.getCacheDir().getPath()得到,当然缓存的地址可以自己随意指定,但是建议使用该方法获取,便于程序管理
版本号可以使用context.getPackageManager().getPackageInfo(context.getPackageName(), 0)来获取
2.写入缓存
写入缓存要使用DiskLruCache.Editor这个类
public Editor edit(String key) throws IOException
它需要传入一个字符串参数,该参数会成为缓存文件的文件名,并且必须是和图片的URL的对应的
考虑到有些图片的URL可能存在一些特殊符号,这样创建文件的时候可能会出问题,所以你需要一种独一无二的不会出错的命名方法,你可以使用MD5编码
private String hashKeyFromUrl(String url){
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = byteToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String byteToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i ++){
String hex = Integer.toHexString(0xFF & bytes[i]);//得到十六进制字符串
if (hex.length() == 1){
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
有了editor对象之后便可以调用它的newOutputStream来获取一个输出流
OutputStream outputStream = editor.newOutputStream(0);
传入的0代表的是下标,因为通常一个key对应着一个文件
得到输出流之后我们便可以写入缓存了,最后调用一下commit使写入生效
private boolean downLoadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
bis = new BufferedInputStream(urlConnection.getInputStream(),8 * 1024);
bos = new BufferedOutputStream(outputStream,8 * 1024);
int b ;
while((b = bis.read())!= -1){
bos.write(b);
}
return true;
} catch (IOException e) {
e.printStackTrace();
}finally {
if (urlConnection != null){
urlConnection.disconnect();
}
closeIn(bis) ;
closeOut(bos);
}
return false;
}
if (downLoadUrlToStream(url,outputStream)){
editor.commit();//提交
}else {
editor.abort();//取消操作
}
就这样把缓存写入了
3.查找缓存
查找缓存同样也需要把url转换成md5码,然后使用DiskLruCache.get方法,得到一个DiskLruCache.Snapshot对象,调用它的getInputStream方法便可以获取输入流了,之后可以使用BitmapFactory.decodeStream()来获取bitmap对象,也可对图像进行缩放BitmapFactory.decodeFileDescriptor(),最后加入到内存缓存中
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null){
FileInputStream fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fis.getFD();
bitmap = imageResizer.decodeBitmapFromFileDescriptor(fileDescriptor,targetWidth,targetHeight);
if (bitmap != null){
addBitmapToMemoryCache(key,bitmap);
}
}
4.移除缓存
同样非常简单,传入url转换后的key,调用remove方法
public synchronized boolean remove(String key) throws IOException
这里就不演示了
我们也许会给程序添加一个清楚缓存的功能,那么就需要使用size方法和delete方法了
size方法可以获得缓存文件总大小,delete方法可以删除所有缓存文件
5.journal文件
如果你打开你的缓存路径,你会发现这个文件,该文件记录了你对缓存空间的添加删除读取操作
libcore.io.DiskLruCache
1
1
1
DIRTY eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa
CLEAN eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa 90642
READ eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa
REMOVE eef8643adeeeb8ec5a48eb0aba1671afaca7c1b35b850eb772136df314e753aa
最上面的是固定的,标示着它使用了DiskLruCache硬盘缓存,下面的第一个1是DisLruCache的版本号,第二个1是应用程序的版本号,第三个1是一个key对应一个文件。
在下面的DIRTY表示正在写入,后面的操作会阻塞,避免产生脏数据,后面跟的一大串是key,也就是之前用url生成了md5
CLEAN代表干完了一个,后面可以继续干了,后面的数字代表缓存文件的大小,会根据这个调整和获取缓存大小
READ代表正在读,Remove代表删除。
该文件并不会一直变大,在程序中有控制,只要到达2000条记录就清理一次记录
三、一个完整的ImageLoader
public class ImageLoader {
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;// 50MB
private Context mContext;
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private boolean mIsDiskLruCacheCreated = false;//用来标记mDiskLruCache是否创建成功
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT+ 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10;
private final int DISK_CACHE_INDEX = 0;
private static final int MESSAGE_POST_RESULT = 101;
private ImageResizer imageResizer = new ImageResizer();
private static final ThreadFactory mThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"ImageLoader#"+mCount.getAndIncrement());
}
};
/**
* 创建线程池
*/
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(),mThreadFactory
);
/**
* 创建Handler
*/
private Handler mHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MESSAGE_POST_RESULT){
LoaderResult loadResult = (LoaderResult) msg.obj;
ImageView iv = loadResult.iv;
String url = (String) iv.getTag();
if (url.equals(loadResult.uri)){//防止加载列表形式时,滑动复用的错位
iv.setImageBitmap(loadResult.bitmap);
}
}
}
};
private ImageLoader(Context mContext) {
this.mContext = mContext.getApplicationContext();
init();
}
/**
* 创建一个ImageLoader
*/
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
/**
* 初始化
* LruCache<String,Bitmap> mMemoryCache
* DiskLruCache mDiskLruCache
*/
private void init() {
// LruCache<String,Bitmap> mMemoryCache
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
return bitmap.getByteCount() / 1024;
}
};
// DiskLruCache mDiskLruCache
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 加载原始大小的图
*/
public void bindBitmap(String uri,ImageView iv){
bindBitmap(uri,iv,0,0);
}
/**
* 异步加载网络图片 指定大小
*/
public void bindBitmap(final String uri, final ImageView iv, final int targetWidth, final int targetHeight){
iv.setTag(uri);
Bitmap bitmap = loadBitmapFormMemCache(uri);
if (bitmap != null){
iv.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri,targetWidth,targetHeight);
if (bitmap != null){
LoaderResult result = new LoaderResult(iv,uri,bitmap);
Message message = mHandler.obtainMessage();
message.obj = result;
message.what = MESSAGE_POST_RESULT;
mHandler.sendMessage(message);
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
/**
* 同步加载网络图片
*/
private Bitmap loadBitmap(String url, int targetWidth, int targetHeight) {
Bitmap bitmap = loadBitmapFormMemCache(url);
if (bitmap != null) {
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(url, targetWidth, targetHeight);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(url, targetWidth, targetHeight);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {//缓存文件夹创建失败
bitmap = downLoadFromUrl(url);
}
return bitmap;
}
/**
* 向缓存中添加Bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 通过key拿到bitmap
*/
private Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
private Bitmap loadBitmapFormMemCache(String url) {
final String key = hashKeyFromUrl(url);
return getBitmapFromMemoryCache(key);
}
/**
* 从网络进行请求
*/
private Bitmap loadBitmapFromHttp(String url, int targetWidth, int targetHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("UI 线程不能进行网络访问");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downLoadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();//重复操作
}
mDiskLruCache.flush();//刷新
}
return loadBitmapFromDiskCache(url, targetWidth, targetHeight);
}
/**
* 从硬盘缓存中读取Bitmap
*/
private Bitmap loadBitmapFromDiskCache(String url, int targetWidth, int targetHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("硬盘读取Bitmap在UI线程,UI 线程不进行耗时操作");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fis.getFD();
bitmap = imageResizer.decodeBitmapFromFileDescriptor(fileDescriptor, targetWidth, targetHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
/**
* 将数据请求到流之中
*/
private boolean downLoadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
bis = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bos = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
closeIn(bis);
closeOut(bos);
}
return false;
}
/**
* 直接通过网络请求图片 也不做任何的缩放处理
*/
private Bitmap downLoadFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream bis = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
bis = new BufferedInputStream(urlConnection.getInputStream());
bitmap = BitmapFactory.decodeStream(bis);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
closeIn(bis);
}
return bitmap;
}
/**
* 得到MD5值key
*/
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = byteToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
/**
* 将byte转换成16进制字符串
*/
private String byteToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);//得到十六进制字符串
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* 得到缓存文件夹
*/
private File getDiskCacheDir(Context mContext, String uniqueName) {
//判断储存卡是否可以用
boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = mContext.getExternalCacheDir().getPath();//储存卡
} else {
cachePath = mContext.getCacheDir().getPath();//手机自身内存
}
return new File(cachePath + File.separator + uniqueName);
}
/**
* 得到可用空间大小
*/
private long getUsableSpace(File file) {
return file.getUsableSpace();
}
/**
* 关闭输入流
*/
private void closeIn(BufferedInputStream in) {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
in = null;
}
}
}
/**
* 关闭输输出流
*/
private void closeOut(BufferedOutputStream out) {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
out = null;
}
}
}
private static class LoaderResult {
public ImageView iv ;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView iv, String uri, Bitmap bitmap) {
this.iv = iv;
this.uri = uri;
this.bitmap = bitmap;
}
}
}