首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备1:HashMap(JDK1.8)原理以及源码分析
面试必备2:JDK1.8LinkedHashMap实现原理及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
缓存是一种通用的思想可以用在很多场景中,但在实际的开发中经常用于Bitmap的缓存,用于提高图片的加载效率、提升产品的用户体验和节省用户流量。目前常见的缓存策略是LruCache和DiskLruCachey用它们分别实现内存缓存和硬盘缓存。Lru是Least Recently Used的缩写即最近最少使用算法,这种算法思想是:当缓存容量快满时,会删除最近做少使用的缓存对象。在这里提醒大家一句三级缓存一般针对的是加载网络图片时常用的缓存策略。
附上我自定义的ImageLoader的代码地址去理解三级缓存原理:https://github.com/mayanhu/ImageLoader
当我们要加载一张网络上的图片时一般流程:
因为LruCache内部采用LinkedHashMap以强引用的形式缓存外界的对象,所以在讲LruCache前需要先了解Java对象(即堆内存中对象)引用的四个级别以及各自的特点,以便我们能更好的掌握内存缓存LruCache的实现原理。
首先简单说一下三级缓存为什么优先从内存中加载:
LruCache内部采用LinkedHashMap以强引用的形式缓存外界的对象,就是以键值对的形式缓存我们的Bitmap对象。LruCache是一个范型类,它是线程安全的因为它对数据的操作都加了锁。它的使用也很简单代码如下:
LruCache mLruCache;
public void init(){
//当前进程的可用内存
int mxaMeory=(int)(Runtime.getRuntime().maxMemory()/1024);
//当前进程的可用内存/8为缓存大小
int meoryChache=mxaMeory/8;
//初始化缓存大小 复写sizeOf计算缓存对象的大小,单位要和总容量的一直
mLruCache=new LruCache(meoryChache){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes()*bitmap.getHeight()/1024;//单位要和meoryChache保持一致
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
//此方法有需要是复写,做一些资源回收动做,在LruCache移除最近最少使用的对象时自动调用
//比如为了提高内存缓存的效率,我可在用一个弱引用的LinkHashMap去存储不常使用的对象,
//实现进一步的缓存
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
}
除了创建外LruCache还提供了:
//存储
mLruCache.put(key,value);
//获取
// mLruCache.get(key);
删除
// mLruCache.remove(key)
第二级缓存,硬盘缓存DiskLruCache,它是通过将缓存对象写入文件系统从而实现缓存,需要注意的是DiskLruCache它不是AndroidSDK的源码,但是他得到了官方文档的推荐,使用它时需要从如下网址下载:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java (注意需要)。
DiskLruCache不能通过构造方法创建,它提供了open方法用于创建自身,如下是open()方法的源码:
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory 缓存目录
* @param appVersion 表示应用版本号一般设置为1即可,当其发生变化时DiskLruCache会清空之前所有的缓存文件
* @param valueCount 表示一个键对应几个值,即一个缓存Key对应几个缓存文件, 一般设置为1即可
* @maxSize 最大缓存容量
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
//初始化如果存在则执行文件尾加操作,否者创建Dir创建在创建DiskLruCache的实例
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
DiskLruCache的缓存操作是通过Editor完成,通过缓存key区获取Editor对象,Editor表示缓存对象的编辑对象,通过Editor对象获取一个输出流指向缓存文件,实现添加缓存操作,这里需要注意的是图片的缓存一般不直接使用该图片的URL,因为URL中有可能有特殊字符影响使用。一般采用URL的MD5作为key。添加缓存操作代码如下
1: 先去获取缓存key
/**
* 根据url 获取MD5key 因为url中可能含有特殊字符 影响在Android中直接使用
* @param url
* @return
*/
private String hashKeyFormUrl(String url){
String cacheKey;
try {
MessageDigest messageDigest=MessageDigest.getInstance("MD5");
messageDigest.update(url.getBytes());
cacheKey=bytesToHexString(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
cacheKey=String.valueOf(url.hashCode());
}
return cacheKey;
}
/**
*
* @param digest
* @return
*/
private String bytesToHexString(byte[] digest) {
StringBuilder sb=new StringBuilder();
for (int i = 0; i < digest.length; i++) {
String hex=Integer.toHexString(0xFF & digest[i]);
if (hex.length()==1){
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
2:通过缓存key获取Editor对象实现添加操作:
/**
* 添加一个缓存图片
* @param urlKey
*/
public void addDiskCacheEdit(String urlKey){
if (mDiskLruCache==null) {
return;
}
String key = hashKeyFormUrl(url);
try {
//通过key拿到edit对象---》outputStream
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
if (edit != null) {
OutputStream outputStream = edit.newOutputStream(DISK_CACHE_INDEX);
//因为open方法中设置一个键对应一个值,所以DISK_CACHE_INDEX一设置为0 即可
//如果文件下载成功提交编辑
if (downloadUrlToStream(url,outputStream)){
//提交写操作进行提交到文件系统
edit.commit();
}else {
//图片下载异常 回退整个操作
edit.abort();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 下载图片资源时,将期存入硬盘缓存目录
* @param urlImage
* @param outputStream
* @return
*/
private boolean downloadUrlToStream(String urlImage, OutputStream outputStream) {
HttpURLConnection urlConnection=null;
BufferedInputStream in=null;
BufferedOutputStream out=null;
try {
URL url=new URL(urlImage);
urlConnection= (HttpURLConnection) url.openConnection();
in=new BufferedInputStream(urlConnection.getInputStream());
out =new BufferedOutputStream(outputStream);
int b;
while ((b=in.read())!=-1){
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
release(urlConnection,in,out);
}
return false;
}
和图片的添加操作类似,查找操作也是需要将url转换为key,然后通过DiskLruCache的get(String key)获取Snapshot对象,通过Snapshot获取缓存文件的输入流,通过输入流去获取缓存的Bitmap对象去使用,只是在使用时存入了内存缓存中。下面是我的获取硬盘缓存的代码:
/**
* 获取一个硬盘缓存图片 并且通过控件宽高进行缩放加载 避免OOM
* @param imageUrl
* @return
*/
public Bitmap getBitmapChache(String imageUrl,int reqWith,int reqHeight){
Bitmap bitmap=null;
String keyFormUrl = hashKeyFormUrl(imageUrl);
FileInputStream inputStream=null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keyFormUrl);
if (snapshot != null) {
//inputStream是一种有序的文件流,通过Options缩放存在问题,两次decodeStream影响了文件流的位置属性,第二次decodeStream时得到的的为null
inputStream= (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
//解决方案是通过文件夹流来得到对应文件的描述符让后通过BitmapFactory.decodeFileDescriptor来加载一张缩略图
FileDescriptor fileDescriptor = inputStream.getFD();
bitmap=BitmapUtils.decodeSampledBitmapFileDescriptor(fileDescriptor,reqWith,reqHeight);
if (bitmap != null) {
// TODO: 2019/3/6 添加到内存缓存 自定义ImageLoader
}
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}
/**
* 文件夹流
* @param fileDescriptor 文件夹流
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampledBitmapFileDescriptor(FileDescriptor fileDescriptor,int reqWidth,int reqHeight){
BitmapFactory.Options options=new BitmapFactory.Options();
//inJustDecodeBounds=true只会解析图片的原始宽高信息,不会真正的去加载图片
options.inJustDecodeBounds=true;
//第一次decode 加载原始图片数据
BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
//计算缩放比例
options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
//重新设置加载图片
options.inJustDecodeBounds=false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
}
/**
* 删除指定的缓存文件
* @param url
*/
private void reloveDiskCache(String url){
String key = hashKeyFormUrl(url);
try {
mDiskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 删除所有的缓存文件
* @param
*/
private void clearALLCache(){
try {
mDiskLruCache.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
这里只是简单的描述了三级缓存的的原理和使用, 它也是现在主流的图片加载框架GLide、ImageLoader实现图片缓思想,只是它们的每一步做的更加精密严谨,使用更加方便,内部使用了多种设计模式实现了代码的高度解耦。