学习资料:
Android 开发艺术探索
其实就是完完全全摘抄,读书笔记 : )
LruCache
和DiskLruCache
是采用了LRU(Least Recently Used)
近期最少使用算法的两种缓存。LruCache
内存缓存,DiskLruCache
存储设备缓存
1.LruCache 内存缓存
LruCache
是一个泛型类,内部是一个LinkedHashMap
以强引用的方式存储缓存对象,提供了get
和put
方法进行对缓存对象的操作。当缓存满时,移除近期最少使用的缓存对象,添加新的缓存对象
- 强引用:直接的对象引用
- 软引用:当一个对象只有软引用存在时,系统内存不足时,会被gc回收
- 弱引用:当一个对象只有弱引用存在时,该对象会随时被gc回收
LruCache
是线程安全的
LruCache
典型的初始化,重写sizeOf()
方法
int MaxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);// kB
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(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 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(),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 mMemoryCache
* DiskLruCache mDiskLruCache
*/
private void init() {
// LruCache mMemoryCache
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(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
原理。
最近的学习,感觉不会的东西太多了,得理一理学习的顺序。
注意多锻炼身体。 共勉:)