Android通过两级缓存加载图片
前一段时间刚好学习用两级缓存(LruCache和DiskLruCache)加载图片问题,在书上看到一个解决方案思路很清晰易懂,在这里记录一下。
整体思路:
当程序从网络加载图片后,为了节省流量,将其缓存到存储设备上,为了提高用户体验通常还会缓存一份到内存,这样当程序再次请求图片时,首先从内存获取,如果内存中没有就从存储设备中获取,如果存储设备中也没有再从网络上获取。
一、采样率
首先在加载图片的时候涉及到一个问题,就是图片尺寸问题,很多时候ImageView并没有图片原始的尺寸那么大,所以把整个图片加载进来再给ImageView是没有必要的,通过BitmapFactory.Options可以按一定的采样率来加载缩小后的图片,这样就会降低内存的占用,从而在一定程度上避免OOM。
下面是计算采样率的代码,相关解释已经都写在了注释中了:
public class ImageResizer {
private static final String TAG = "ImageResizer";
/**
* 该方法主要用到了BitmapFactory.Options中的采样率inSampleSize参数
* inSampleSize = 1时,采样后图片大小为图片的原始大小;
* inSampleSize = 2时,采样后的图片宽高均为原图的1/2,像素数为原图的1/4,占用内存也是原来的1/4;
* inSampleSize < 1时,作用相当于1,即无缩放效果;
* inSampleSize的取值总是2的指数,如果传入的值不是2的指数,
* 系统会向下取整并选择一个接近2的指数代替,但不是所有的android版本都成立;
* @param res 资源值
* @param resId 需要加载的图片id
* @param reqWidth 需要加载的图片宽度
* @param reqHeight 需要加载的图片高度
* @return 根据实际需要缩放后的图片
*/
public static Bitmap decodeSampledBitmapFromResource(
Resources res, int resId, int reqWidth, int reqHeight) {
//通过BitmapFactory.Options来加载所需尺寸的图片
final BitmapFactory.Options options = new BitmapFactory.Options();
/**
*当inJustDecodeBounds=true时,
* BitmapFactory只会解析图片的原始宽/高信息,
* 不会真正加载图片
*/
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//计算inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampleBitmapFromFielDescriptor(
FileDescriptor fd, int reqWidth, int reqHeight){
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, 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;
}
}*/
while (height/inSampleSize > reqHeight && width/inSampleSize > reqWidth){
inSampleSize *= 2;
}
return inSampleSize;
}
}
二、两级缓存的使用
1、LruCache
LruCache是Android3.1提供的一个缓存类,通过support-v4兼容包可以兼容到早期版本,不过现在Android3.1以下的版本好像几乎没有了。。。。,LruCache是泛型类,其内部采用LinkedHashMap以强引用的方式存储外界缓存对象,如果不了解强引用、软引用、弱引用、虚引用的请自行google/百度。
/*
LruCache初始化
*/
//当前进程可用内存,单位KB
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//设置缓存大小为当前进程可用内存的1/8
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
2、DiskLruCache
DiskLruCache不属于android sdk的一部分,所以需要自行下载源码并稍微做一下改动就可使用,后面我会附上我使用的源码。
/*
DiskLruCache初始化
*/
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 class ImageLoader {
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
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 = 10L;
/*指定的密钥应该是在应用程序的资源中声明的id,
以确保它是唯一的(请参阅ID资源类型)。
标识为属于Android框架的键或与任何程序包无关的键将导致IllegalArgumentException被抛出*/
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(@NonNull 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 LinkedBlockingQueue<Runnable>(), sThreadFactory);
private Context mContext;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache; //内部实现是LinkedHashMap
private DiskLruCache mDiskLruCache;
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (result.uri.equals(uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "bitmap的uri被修改");
}
}
};
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
/*
LruCache初始化
*/
//当前进程可用内存,单位KB
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//设置缓存大小为当前进程可用内存的1/8
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
/*
DiskLruCache初始化
*/
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();
}
}
}
/**
* 创建一个ImageLoader实例
*
* @param context
* @return
*/
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
/**
* 给ImageView添加相应的图片
*
* @param uri
* @param imageView
* @param reqWidth
* @param reqHeight
*/
public void bindBitmap(
final String uri, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
/**
* 从内存、硬盘或者网络中加载图片
* 如果内存中能找到,返回bitmap,
* 如果内存中找不到,从硬盘中加载;
* 如果硬盘中能找到,返回bitmap,
* 如果硬盘中找不到,从网络中加载;
* 如果创建了磁盘缓存,则将图片下载到硬盘中,
* 如果没有创建磁盘缓存,则直接将图片加载到内存中
*
* @param uri
* @param reqWidth
* @param reqHeight
* @return
*/
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && !mIsDiskLruCacheCreated) {
downloadBitmapFromUrl(uri);
}
return bitmap;
}
/**
* 从根据图片的哈希码内存缓存中取出图片
*
* @param key
* @return
*/
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
/**
* 如果内存缓存中没有图片,向其中加入该图片
*
* @param key
* @param bitmap
*/
private void addBitmapToMemCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 从内存缓存中加载图片
*
* @param url
* @return
*/
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormString(url);
return getBitmapFromMemCache(key);
}
/**
* 从磁盘缓存中加载图片
*
* @param url
* @param reqWidth
* @param reqHeight
* @return
* @throws IOException
*/
private Bitmap loadBitmapFromDiskCache(
String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "不推荐使用UI线程加载图片");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormString(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream =
(FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFielDescriptor(
fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemCache(key, bitmap);
}
}
return bitmap;
}
/**
* 从网络中加载图片
*
* @param url 图片地址
* @param reqWidth 所需宽度
* @param reqHeight 所需高度
* @return 所需的图片
* @throws IOException
*/
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("不能在主线程中进行联网操作!");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormString(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, reqWidth, reqHeight);
}
/**
* 将网络图片通过输出流输出
*
* @param urlString 需要下载图片的地址
* @param outputStream 输出流
* @return 下载成功返回真,否则返回假
*/
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
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 n;
while ((n = in.read()) != -1) {
out.write(n);
}
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 直接将网络中的图片加载到内存中,赋值给bitmap变量
*
* @param urlString 网络图片的地址
* @return 返回下载的图片
*/
private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
/**
* 获取储存图片的路径,如果挂载了sd卡则目录为sd卡的路径,否则用缓存
*
* @param context 上下文
* @param uniqueName 文件名
* @return 返回文件
*/
public File getDiskCacheDir(Context context, String uniqueName) {
//判断是否挂载sd卡
boolean externalStorageAvailable =
Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) { //如果挂载了sd卡,则使用sd卡的路径
cachePath = context.getExternalCacheDir().getPath();
} else { //没有挂载sd卡,使用缓存中的路径
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
/**
* 得到可用空间的大小
*
* @param path 文件
* @return 可用空间大小
*/
private long getUsableSpace(File path) {
//当api<9时使用如下方法
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
final StatFs statFs = new StatFs(path.getPath());
return (long) statFs.getBlockSize() * (long) statFs.getAvailableBlocks();
}
return path.getUsableSpace();
}
/**
* 将字符串转换成MD5码
*
* @param str 待转换的字符串
* @return 转换成功返回MD5码,否则返回哈希码
*/
private String hashKeyFormString(String str) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(str.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(str.hashCode());
}
return cacheKey;
}
/**
* 将字节数组转换成16进制形式字符串
*
* @param bytes 字节数组
* @return 16进制形式字符串
*/
private String bytesToHexString(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 static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}