Android 缓存LruCache和DiskLruCache
Posted 花姓-老花
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 缓存LruCache和DiskLruCache相关的知识,希望对你有一定的参考价值。
Lru缓存算法
Lru(Least Recently Used)翻译过来就是最近最少使用意思,Lru其核心思想就是当缓存存满时,优先删除最近最少使用的缓存对象。
Lru缓存方式:
1、LruCache 用于实现手机内存缓存
2、DiskLruCache 用于实现外置内存缓存(它不属于官方sdk的一部分,但得到官方的推荐)
LruCache
LruCache是android3.1提供的,使用support-4兼容包中LruCache可以向下兼容。
LruCache是一个泛型类,其内部采用LinkedHashMap以强引用的方式来存储外界的缓存对象,提供了put和get方法来添加和获取缓存对象;当缓存满时,Lrucache会移除较早使用的缓存对象,然后再添加新的缓存对象,LruCache是线程安全的。
复习一下知识点:
强引用:只要引用的存在,垃圾回收器永远不会被回收
案例:Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());
而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
软引用:非必须引用,内存溢出之前就进行回收
案例:Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有的弱引用的对象。
案例:Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
案例: Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。
LruCahce使用
Runtime.getRuntime().maxMemory()在Java中返回的是java虚拟机(这个进程),能够从操作系统那里获取最大的内存,而在Android中返回应用程序最大的可用内存,单位是字节。
还可用((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass()来获取,单位是M。
Runtime.getRuntime().totalMemory()在Java中返回的是Java虚拟机现在已经从操作系统那里获取的内存大小,也就是Java虚拟机这个进程当时所占用的所有内存,在Android中返回的是应用程序已获得内存,所以totalMemory()是慢慢增大的。
Runtime.getRuntime().freeMemory()就是已经获取到但还没有使用的内存。
LruCache运用案例
String mUrl = "http://img5.imgtn.bdimg.com/it/u=274881988,3237971911&fm=27&gp=0.jpg";
ImageLoader.getInstance().showImage(mImage,mUrl);
ImageLoader工具类
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.v4.util.LruCache;
import android.widget.ImageView;
import android.widget.ListView;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
/**
* Created by hjz on 2018/4/16 0016.
* 图片处理类
*/
public class ImageLoader
private static ImageLoader instance;
//LruCache缓存对象
private LruCache<String,Bitmap> mCache;
//下载任务的集合
private Set<ImageLoaderTask> mTask;
public static ImageLoader getInstance()
if (instance == null)
instance = new ImageLoader();
return instance;
public ImageLoader()
mTask = new HashSet<>();
//获取最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//设置缓存大小
int cacheSize = maxMemory / 8;
mCache = new LruCache<String, Bitmap>(cacheSize)
@Override
protected int sizeOf(String key, Bitmap value)
//在每次存入缓存的时候调用
return value.getByteCount();
;
/**
* 将bitmap添加到缓存中
* @param url LruCache的key值,即是图片下载的路径
* @param bitmap LruCache的值,即是图片bitmap对象
*/
public void addBitmapToCache(String url,Bitmap bitmap)
if (getBitmapFromCache(url) == null)
mCache.put(url,bitmap);
/**
* 从缓存中获取bitmap对象
* @param url LruCache的key
* @return 返回bitmap对象
*/
public Bitmap getBitmapFromCache(String url)
if (mCache == null)
return null;
Bitmap bitmap = mCache.get(url);
return bitmap;
/**
* 加载bitmap对象
* @param start 第一个可见的ImageView的下标
* @param end 最后一个可见的ImageView的下标
*/
public void showImages(int start,int end,ListView listView)
for (int i=0; i<end; i++ )
String imageUrl = "";//ImageAdapter.URLS[i];
//从缓存中获取图片
Bitmap bitmap = getBitmapFromCache(imageUrl);
//如果缓存中没有,则去下载
if (bitmap == null)
ImageLoaderTask task = new ImageLoaderTask(listView,imageUrl);
task.execute();
mTask.add(task);
else
if (listView != null)
ImageView imageView = (ImageView) listView.findViewWithTag(imageUrl);
imageView.setImageBitmap(bitmap);
/**
* 显示图片(单张图片)
* @param imageView
* @param imageUrl
*/
public void showImage(ImageView imageView,String imageUrl)
//从缓存中取图片
Bitmap bitmap = getBitmapFromCache(imageUrl);
//如果缓存中没有,则去下载
if (bitmap == null)
ImageLoaderTask task = new ImageLoaderTask(imageView,imageUrl);
task.execute();
else
imageView.setImageBitmap(bitmap);
/**
* 取消所有下载任务
*/
public void cancelAllTask()
if (mTask != null)
for (ImageLoaderTask task : mTask)
task.cancel(false);
/**
* 下载并显示图片
*/
private class ImageLoaderTask extends AsyncTask<Void, Void, Bitmap>
private ImageView mImageView;
private ListView mListView;
private String mImageUrl;
ImageLoaderTask(String imageUrl)
mImageUrl = imageUrl;
ImageLoaderTask(ListView listView,String imageUrl)
mImageUrl = imageUrl;
mListView = listView;
ImageLoaderTask(ImageView imageView, String imageUrl)
mImageView = imageView;
mImageUrl = imageUrl;
@Override
protected Bitmap doInBackground(Void... params)
Bitmap bitmap = getBitmapByImageUrl(mImageUrl);
if (bitmap != null)
addBitmapToCache(mImageUrl, bitmap);
return bitmap;
@Override
protected void onPostExecute(Bitmap bitmap)
super.onPostExecute(bitmap);
if (mListView != null)
ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl);
if (imageView != null && bitmap != null)
imageView.setImageBitmap(bitmap);
else if (mImageView != null)
if (bitmap != null)
mImageView.setImageBitmap(bitmap);
mTask.remove(this);
/**
* 根据图片路径下载图片Bitmap
* @param imageUrl 图片网络路径
* @return
*/
private Bitmap getBitmapByImageUrl(String imageUrl)
Bitmap bitmap = null;
HttpURLConnection connection = null;
try
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(20*1000); //建立连接的超时时间10s【只有在网络正常的情况下才有效】
connection.setReadTimeout(20*1000);//传递数据的超时时间10s【网络不正常时才真正的起作用】
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
catch (MalformedURLException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
finally
if (connection != null)
connection.disconnect();
return bitmap;
DiskLruCache
在使用DiskLruCache时,首先要从网上下载DiskLruCache的源码下来,然后放在自己项目中编译后才能正常使用。
DiskLruCache使用
1、DiskLruCache创建
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
第一个参数:directory表示磁盘缓存的存储路径缓存目录没有具体限制,可以根据需求自己的定义。一般来说,可以选择SD卡上的/sdcard/Android/data/<application package>/cache目录,这个目录是Android系统指定的应用程序缓存目录,当应用卸载时,缓存也会被系统清除;当然还可以选择sd卡上的其他目录,也可以选择data下的当前应用目录。当然,一个严禁的程序还要考虑SD卡是否存在等
第二个参数:appVersion表示应用的版本号
当appVersion改变时,之前的缓存都会被清除,所以如非必要,我们为其指定一个1,不再改变即可
第三个参数:valueCount表示单个节点对应的数据个数,也就是同一个key可以对应多少个缓存文件,一般来说我们都选取1
第四个参数:maxSize缓存的总大小
2、DiskLruCache 添加缓存
首先将url转换为key
/**
* 用hash将url转换成对应的key值
* @param key
* @return
*/
public static String hashKeyForDisk(String key)
String cacheKey;
try
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
catch (NoSuchAlgorithmException e)
cacheKey = String.valueOf(key.hashCode());
return cacheKey;
/**
* 将字节数组 转换成 十六进制 的字符串
* @param bytes 摘要内容
* @return
*/
private static String bytesToHexString(byte[] bytes)
// http://stackoverflow.com/questions/332079
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();
然后通过DiskLruCache的get方法得到一个Snapshot对象,再通过Snapshot对象可得到缓存的文件输入流
/**
* bitmap相关操作
* @param data 网络图片url
* @return
*/
private Bitmap processBitmap(String data)
if (BuildConfig.DEBUG)
Log.d(TAG, "processBitmap - " + data);
final String key = ImageCache.hashKeyForDisk(data); //通过URL获取key值
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
DiskLruCache.Snapshot snapshot;
synchronized (mDiskCacheLock)
// Wait for disk cache to initialize
while (mDiskCacheStarting)
try
mDiskCacheLock.wait();
catch (InterruptedException e)
if (mDiskLruCache != null)
try
//通过DiskLruCache的get方法 得到 Snapshot对象
snapshot = mDiskLruCache.get(key);
//snapshot不存在,则下载并保持到DiskLruCache
if (snapshot == null)
if (BuildConfig.DEBUG)
Log.d(TAG, "processBitmap, not found in http cache, downloading...");
DiskLruCache.Editor editor = mDiskLruCache.edit(key); //对DiskLruCache进行操作
if (editor != null)
//下载图片
if (downloadUrlToStream(data,editor.newOutputStream(DISK_CACHE_INDEX)))
editor.commit(); //提交
else
editor.abort(); //中断
snapshot = mDiskLruCache.get(key);
//存在则直接获取
if (snapshot != null)
fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
fileDescriptor = fileInputStream.getFD();
catch (IOException e)
Log.e(TAG, "processBitmap - " + e);
catch (IllegalStateException e)
Log.e(TAG, "processBitmap - " + e);
finally
if (fileDescriptor == null && fileInputStream != null)
try
fileInputStream.close();
catch (IOException e)
Bitmap bitmap = null;
if (fileDescriptor != null)
bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,
mImageHeight, getImageCache());
if (fileInputStream != null)
try
fileInputStream.close();
catch (IOException e)
return bitmap;
/**
* 下载URL到流
* @param urlString 网络图片地址
* @param outputStream 写到的流中
* @return
*/
private boolean downloadUrlToStream(String urlString, OutputStream outputStream)
disableConnectionReuseIfNecessary();
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1)
out.write(b);
return true;
catch (Exception e)
Log.e(TAG, "Error in downloadBitmap - " + e);
finally
if (urlConnection != null)
urlConnection.disconnect();
try
if (out != null)
out.close();
if (in != null)
in.close();
catch (final IOException e)
return false;
为了避免图片造成内存溢出(OOM),则对图片进行缩放
/**
* 对图片进行缩放
* @param fileDescriptor FileDescriptor是文件描述符,若需要通过FileDescriptor对该文件进行操作,
* 则需要新创建FileDescriptor对应的FileOutputStream,
* 再对文件进行操作
* @param reqWidth 用户期待图片宽
* @param reqHeight 用户期待图片高
* @param cache
* @return
*/
public static Bitmap decodeSampledBitmapFromDescriptor(
FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache)
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
// 计算 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
// If we're running on Honeycomb or newer, try to use inBitmap
if (SdkVersionUtils.hasHoneycomb())
addInBitmapOptions(options, cache);
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
/**
* 缩放比率
* @param options
* @param reqWidth 用户期待图片宽
* @param reqHeight 用户期待图片高
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight)
// 原始图像的高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth)
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth)
inSampleSize *= 2;
// This offers some additional logic in case the image has a strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might still
// end up being too large to fit comfortably in memory, so we should
// be more aggressive with sample down the image (=larger inSampleSize).
long totalPixels = width * height / inSampleSize;
// Anything more than 2x the requested pixels we'll sample down further
final long totalReqPixelsCap = reqHeight * reqWidth * 2;
while (totalPixels > totalReqPixelsCap)
inSampleSize *= 2;
totalPixels /= 2;
return inSampleSize;
3、DiskLruCache 从缓存中查(上面代码中)
//存在则直接获取
if (snapshot != null)
fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
fileDescriptor = fileInputStream.getFD();
4、DiskLruCache 缓存的删除
private LruCache<String,BitmapDrawable> mMemoryCache;
public void clearCache()
if (mMemoryCache != null)
mMemoryCache.evictAll();
if (BuildConfig.DEBUG)
Log.d(TAG, "Memory cache cleared");
synchronized (mDiskCacheLock)
mDiskCacheStarting = true;
if (mDiskLruCache != null && !mDiskLruCache.isClosed())
try
mDiskLruCache.delete();
if (BuildConfig.DEBUG)
Log.d(TAG, "Disk cache cleared");
catch (Exception e)
Log.e(TAG, "clearCache - " + e);
mDiskLruCache = null;
initDiskCache();
5、DiskLruCache 其他几个重要且常用的方法
1、size() 这个方法返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以调用这个方法计算缓存图片大小。
2、flush()用于将内存中操作同步到日志文件(joural文件)当中,DiskLruCache能正常工作的前提就是要依赖journal文件的内容;前面所说的写入缓存操作时调用一次flush()方法,但不是每次吸入缓存都要调用一次flush()方法,因为频繁调用额外添加了同步journal文件时间,标准的做法就是在Activity的onPause()方法中去调用一次flush()方法即可。
3、close()用于将DiskLruCache关闭掉,和open()方法对应,关闭掉之后就不能再调用DiskLruCache中任何操作缓存数据的方法,标准做法在Activity的onDestroy()方法中调用close()方法。
4、delete() 用于将所有缓存数据全部删除
6、journal 文件
DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容,因此,能够读懂journal文件对于我们理解DiskLruCache的工作原理有着非常重要的作用。那么journal文件中的内容到底是什么样的呢?我们来打开瞧一瞧吧,如下所示:
libcore.io.DiskLruCache
1
1
1
DIRTY 7a3200a75c9bf6b7517c9ef6faa089d8
DIRTY 7a3200a75c9bf6b7517c9ef6faa089d8
CLEAN 7a3200a75c9bf6b7517c9ef6faa089d8 5690
DIRTY 5281f85549e3639c0e88006e9c94c334
CLEAN 5281f85549e3639c0e88006e9c94c334 7573
DIRTY 2f32ed1d503f10ca4d830b7e7ef3b442
READ 7a3200a75c9bf6b7517c9ef6faa089d8
READ 5281f85549e3639c0e88006e9c94c334
READ 7a3200a75c9bf6b7517c9ef6faa089d8
READ 5281f85549e3639c0e88006e9c94c334
DIRTY 2f32ed1d503f10ca4d830b7e7ef3b442
READ 5281f85549e3639c0e88006e9c94c334
READ 7a3200a75c9bf6b7517c9ef6faa089d8
READ 5281f85549e3639c0e88006e9c94c334
READ 7a3200a75c9bf6b7517c9ef6faa089d8
DIRTY 2f32ed1d503f10ca4d830b7e7ef3b442
由于现在只缓存了一张图片,所以journal中并没有几行日志,我们一行行进行分析。第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着我们使用的是DiskLruCache技术。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行是一个空行。前五行也被称为journal文件的头,这部分内容还是比较好理解的,但是接下来的部分就要稍微动点脑筋了。
第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
如果你足够细心的话应该还会注意到,第八行的那条记录,除了CLEAN前缀和key之外,后面还有一个5690,这是什么意思呢?其实,DiskLruCache会在每一行CLEAN记录的最后加上该条缓存数据的大小,以字节为单位。5690也就是我们缓存的那张图片的字节数。
前面我们所说的size()方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把journal文件中所有CLEAN记录的字节数相加,求出的总合再把它返回而已。前面我们所学的size()方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把journal文件中所有CLEAN记录的字节数相加,求出的总合再把它返回而已。
除了DIRTY、CLEAN、REMOVE之外,还有一种前缀是READ的记录,这个就非常简单了,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。
那么你可能会担心了,如果我不停频繁操作的话,就会不断地向journal文件中写入数据,那这样journal文件岂不是会越来越大?这倒不必担心,DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内。
github: https://github.com/zjws23786/Lru
以上是关于Android 缓存LruCache和DiskLruCache的主要内容,如果未能解决你的问题,请参考以下文章