package xxx.xxx.xxx.xxx;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.util.LruCache;
import android.widget.ImageView;
import com.android.volley.toolbox.ImageLoader;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class BitmapCache implements ImageLoader.ImageCache {
private static final int DISK_CACHE_SIZE = 1024 *1024 * 50;
private static final int DISK_CACHE_INDEX = 0;
private LruCache<String, Bitmap> lruCache;
private DiskLruCache diskLruCache;
public BitmapCache(Context context) {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(context, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
diskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public Bitmap getBitmap(String key) {
Bitmap bitmap = lruCache.get(key);
if (bitmap == null) {
try {
DiskLruCache.Snapshot snapshot = diskLruCache.get(hashKeyForDisk(key));
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
if (bitmap != null) {
lruCache.put(key, bitmap);
}
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
@Override
public void putBitmap(String key, Bitmap bitmap) {
lruCache.put(key, bitmap);
try {
DiskLruCache.Editor editor = diskLruCache.edit(hashKeyForDisk(key));
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)) {
editor.commit();
} else {
editor.abort();
}
diskLruCache.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private File getDiskCacheDir(Context context, String uniqueName) {
final String cachePath;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return stats.getBlockSizeLong() * stats.getAvailableBlocksLong();
}
private 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;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
public static String getCacheKey(String url, ImageView imageView) {
return "#W" + imageView.getWidth()
+ "#H" + imageView.getHeight()
+ "#S" + imageView.getScaleType().ordinal()
+ url;
}
private 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 heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
}
至于我们为什么需要图片缓存,以及什么是内存缓存,什么是磁盘缓存,什么是Volley这些东西我就不想写了,我附上几篇郭神的博客,可以用作参考。
Android Volley完全解析(二),使用Volley加载网络图片
Android高效加载大图、多图解决方案,有效避免程序OOM
Android DiskLruCache完全解析,硬盘缓存的最佳方案
除了这些博客外,《Android开发艺术探索》一书第十二章对图片缓存这一块也讲的比较详细。
还有题外话就是以上的博客和书中所给的DiskLruCache源码的下载地址,下载下来的源码是不能直接编译成功的,还需要使用者自己做一些修改,比较麻烦。但是Android Developer官网给出的源码是直接可用的,我这里直接给出地址(需要科学上网,大家都懂的):
然后简单说一下我的代码,如果使用的话,直接复制粘贴就行了,不过最上面类的包名自己要写下,再一个就是如果DiskLruCache和本类如果不在一个包,还需要import一下。
本类实现了Volley的ImageCache接口,绝大部分实现细节都来自以上教程的整合。
重写接口的getBitmap()方法,大致逻辑为,首先从内存缓存中取出bitmap,如果没有,即当返回null的时候,从磁盘缓存读取,如果从硬盘成功读取,根据LRU算法的近期最少使用的原则,我们应该吧从磁盘读取的bitmap重新加入内存缓存。
重写接口的putBitmap()方法,大致逻辑为,拿到从网络加载来的bitmap以后,先写入内存缓存,再写入磁盘缓存。与以上教程不同的地方为,教程中是把网络请求来的数据流直接写入输出流,但是由于我们这个是Volley的扩展,所以我们要考虑使用Volley的情况,NetworkImageView会自动压缩bitmap,所以我们在使用的时候不必考虑压缩的问题,其次,由于Volley已经做过处理,所以我们直接会从此回调方法中得到bitmap,因此就不是直接从网络数据流写入输出流了,而是把bitmap写入输出流,这里使用了bitmap的compress()方法。
关于缓存使用的键值问题:Volley的键值不仅包含url,还包含宽,高,ScaleType三个信息。原本的键值在内存缓存中使用时没有任何问题的,但是使用在磁盘缓存中会出现问题,因为URL中包含的 “ / ”这个斜杠符号会对存储路径造成影响,导致目录出现问题,所以即使Volley有自己的键值生成的方法我们在使用磁盘缓存时仍需把原键MD5化。关于Volley的键是怎么生成的,我在代码中的最下面三个静态方法中的第一个已经给出。后面两个静态方法是从Resource中使用图片的时候进行压缩的两个工具方法,上面给出的博客中有介绍。
此外,bytesToHexString()方法中的for循环改为了更有逼格的foreach循环(好吧,这没什么值得说的)。