您的位置:首页 > 移动开发 > Android开发

Android 缓存策略LruCache和DiskLruCache学习

2017-07-08 00:12 393 查看


Android 缓存策略LruCache和DiskLruCache学习



 

作者 英勇青铜5 关注

2016.09.05 19:18* 字数 1521 阅读 883评论
7喜欢 21

学习资料:
Android 开发艺术探索

其实就是完完全全摘抄,读书笔记 : )
LruCache
DiskLruCache
是采用了
LRU(Least
Recently Used)
近期最少使用算法的两种缓存。
LruCache
内存缓存,
DiskLruCache
存储设备缓存


1.LruCache 内存缓存

LruCache
是一个泛型类,内部是一个
LinkedHashMap
以强引用的方式存储缓存对象,提供了
get
put
方法进行对缓存对象的操作。当缓存满时,移除近期最少使用的缓存对象,添加新的缓存对象
强引用:直接的对象引用
软引用:当一个对象只有软引用存在时,系统内存不足时,会被gc回收
弱引用:当一个对象只有弱引用存在时,该对象会随时被gc回收
LruCache
是线程安全的
public class LruCahe<K,V>{
private final LinkedHashMap<K,V> map;
...
}


使用final是所谓的安全发布(safe publication)的一种方式

至于
final
修饰后的变量为啥是线程安全的,网上也搜索了一下,看了下,暂时并不理解,先记住,随着以后的学习深入再继续了解
: )
关于java中final关键字与线程安全性 
Java 并发编程(二)对象的不变性和安全的发布对象
以后注意
final
的使用
LruCache
典型的初始化,重写
sizeOf()
方法
int MaxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);// kB
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key,Bitmap bitmap){
//bitmap.getByteCount() = bitmap.getRowBytes() * bitmap.getHeight();
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;// KB
}
}

计算缓存对象大小的单位和总容量的单位要保持一致
一些特殊时候,还需要重写
entryRemoved()
方法。
LruCache
移除旧缓存对象时会调用这个方法,根据需求可以在这个方法中完成一些资源回收工作
获取一个缓存对象,
mMemoryCache.get(key)


添加一个缓存对象,
mMemoryCache.put(key,bitmap)


2.DiskLruCache 磁盘缓存

DiskLruCache
并不是
Android
SDK
中的类。不明白为啥,官方只进行推荐,为何不加入
SDK


2.1创建

private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;// 50MB

File diskCacheDir = new File(mContext,"bitmap");
if(!diskCacheDir.exists()){
diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);

diskCacheDir
 缓存文件夹,具体指
sdcard/Android/data/package_name/cache

1
 应用版本号,一般写为1
1
 单个节点所对应的数据的个数,一般写1
DISK_CACHE_SIZE
 缓存大小


2.2添加

DishLruCache
缓存添加的操作通过
Eidtor
完成,
Editor
为一个缓存对象的编辑对象。
首先需要获取图片的
url
所对应的
key
,根据
key
利用
edit()
来获取
Editor
对象。若此时,这个缓存正在被编辑,
edit()
会返回
null
DiskLruCache
不允许同时编辑同一个缓存对象。之所以把
url
转换成
key
,因为图片的
url
中可能存在特殊字符,会影响使用,一般将
url
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;
}

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();
}

url
转成
key
,利用这
key
值获取
Editor
对象。若这个
key
Editor
对象不存在,
edit()
方法就创建一个新的出来。通过
Editor
对象可以获取一个输出流对象。
DiskLruCache
open()
方法中,一个节点只能有一个数据,
edit.newOutputStream(DISK_CACHE_INDEX)
参数设置为
0

String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor =mDiskLruCache.edit(key);
if (editor != null){
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}

有了这个文件输出流,从网络加载一个图片后,通过这个
OutputStream
outputStream
写入文件系统
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;
}

上面的代码并没有将图片写入文件系统,还需要通过
Editor.commit()
提交写入操作,若写入失败,调用
abort()
方法,进行回退整个操作
if (downLoadUrlToStream(url,outputStream)){
editor.commit();//提交
}else {
editor.abort();//重复操作
}

这时,图片已经正确写入文件系统,接下来的图片获取就不需要请求网络


2.3 缓存查找

查找过程,也需要将
url
转换为
key
,然后通过
DiskLruCache
get
方法得到一个
Snapshot
对象,再通过
Snapshot
对象可得到缓存的文件输入流,有了输入流就可以得到
Bitmap
对象
为了避免
oom
,会使用
ImageResizer
进行缩放。若直接对
FileInputStream
进行操作,缩放会出现问题。
FileInputStream
是有序的文件流,两次
decodeStream
调用会影响文件流的位置属性。可以通过文件流得到其所对应的文件描述符,利用
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);
}
}

在查找得到
Bitmap
后,把
key,bitmap
添加到内存缓存中


3.ImageLoader的实现

主要思路:
拿到图片请求地址
url
后,先把
url
变作对应的
key

利用
key
在内存缓存中查找,查找到了就进行加载显示图片;若没有查到就进行
3

在磁盘缓存中查找,在若查到了,就加载到内存缓存,后加载显示图片;若没有查找到,进行
4

进行网络求,拿到数据图片,把图片写进磁盘缓存成功后,再加入到内存缓存中,并根据实际所需显示大小进行合理缩放显示
类比较长,查看顺序:构造方法->bindBitmap(),之后顺着方法内的方法,结合4个步骤,并不难理解
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;
}
}
}

经过测试,是可以正常加载出图片的,缓存文件也正常,主要是学习过程思路


3.1补充 Closeable接口

ImageLoader
中,代码最后写了
closeIn(),closeOut()
方法,完全没必要这样写。在
jdk1.6
之后,所有的流都实现了
Closeable
接口,可以用这个接口来关闭所有的流
/**
* 关闭流
*/
private void closeStream(Closeable closeable){
if (closeable != null){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

downLoadFromUrl()
downLoadUrlToStream()
方法中需要关闭流,就可以直接
closeStream(bis),closeStream(bos)
来进行关闭流操作


4.最后

这个
ImgageLoader
还非常简陋,和
Glide
Picasso
根本无法相比。并不会在实际工作开发中使用,还是使用
Glide
或者
Picasso
。主要是学习基础实现原理,学习了下
Android
中缓存的部分知识。之前面试时,被问到过
ImageLoader
原理。
最近的学习,感觉不会的东西太多了,得理一理学习的顺序。
注意多锻炼身体。 共勉:)

 Android基础学习

© 著作权归作者所有

举报文章



关注英勇青铜5
写了 138621 字,被 633 人关注,获得了 1088 个喜欢

Android 开发 水平:青铜5

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: