在 UI 界面加载一张图片时很简单,然而如果需要加载多张较大的图像,事情就会变得更加复杂。在许多情况下(如 ListView、RecyclerView 或 ViewPager 等的组件),屏幕上的图片的总数伴随屏幕的滚动会大大增加,且基本上是无限的。为了使内存使用保持在稳定范围内,防止出现 OOM,这些组件会在子 iew 划出屏幕后,对其进行资源回收,并重新显示新出现的图片,垃圾回收机制会释放掉不再显示的图片的内存空间。但是这样频繁地处理图片的加载和回收不利于操作的流畅性,而内存或者磁盘的 Cache 就会帮助解决这个问题,实现快速加载已加载的图片。在缓存上,主要有两种级别的 Cache:LruCache 和 DiskLruCache,即内存缓存与磁盘缓存。我们就来借助内存缓存和磁盘缓存来实现一个简易的图片缓存框架。
概要设计
BitMapLruCache.java:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BitMapLruCache {
private static MemoryCacheUtils memoryCacheUtils;
private static ExecutorService executor;
private static final String TAG = "BitMapLruCache";
private BitMapLruCache(){}
/**
* 外部私有缓存文件夹
*/
private static File cacheDir;
/**
* 初始化BitMapLruCache,必须在App启动完成后调用!
* @param context context
*/
public static void init(Context context){
cacheDir = context.getExternalCacheDir();
memoryCacheUtils = new MemoryCacheUtils();
executor = new ThreadPoolExecutor(
4, 8,
10, TimeUnit.SECONDS, new LinkedBlockingDeque<>(128));
}
/**
* 根据URL获取一张图片
* @param url URL
* @param imageView {@link ImageView}
* @param listener {@link LoadBitMapListener} 加载BitMap的监听器
*/
public static void getBitMap(String url, ImageView imageView, LoadBitMapListener listener) {
// 先从内存找
Bitmap fromMemory = memoryCacheUtils.getBitmapFromMemory(url);
if(fromMemory != null){
listener.success(fromMemory, imageView);
}else {
// 找不到就从磁盘找
Bitmap bitmap = getBitMapFromDisk(url);
if(bitmap != null){
listener.success(bitmap, imageView);
// 找到就保存到内存
memoryCacheUtils.setBitmapToMemory(url, bitmap);
}else{
// 找不到从网络加载 & 保存到内存
GetFromNetWorkTask task = new GetFromNetWorkTask(url, imageView, listener);
executor.submit(task);
}
}
}
/**
* 根据URL从磁盘加载BitMap
* @param url 图片URL
* @return bitmap {@link Bitmap}
*/
private static Bitmap getBitMapFromDisk(String url) {
File file = new File(cacheDir, calcMD5(url));
if(file.exists()){
try {
return BitmapFactory.decodeStream(new FileInputStream(file));
} catch (IOException e) {
Log.e(TAG, "getBitMapFromDisk: ", e);
}
}
return null;
}
/**
* 保存BitMap到磁盘
* @param url 图片URL
* @param bitmap {@link Bitmap}
*/
private static void saveBitMapToDisk(String url, Bitmap bitmap) {
//url convert
try {
File file = new File(cacheDir, calcMD5(url));
Log.i(TAG, "saveBitMapToDisk: file' path = " + file.getAbsolutePath());
FileOutputStream out = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
} catch (IOException e) {
Log.e(TAG, "saveBitMapToDisk: ", e);
}
}
/**
* 加载BitMap过程中的监听器
*/
public interface LoadBitMapListener{
void success(Bitmap bitmap, ImageView imageView);
void failed();
}
/**
* 从网络加载的Task
*/
static private class GetFromNetWorkTask implements Runnable {
String urlStr;
ImageView imageView;
LoadBitMapListener listener;
public GetFromNetWorkTask(String urlStr, ImageView imageView, LoadBitMapListener listener) {
this.urlStr = urlStr;
this.imageView = imageView;
this.listener = listener;
}
@Override
public void run() {
try {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
InputStream connInputStream = conn.getInputStream();
Bitmap baseBitmap = BitmapFactory.decodeStream(connInputStream);
// 比例压缩
Bitmap bitmap = imageCompressL(baseBitmap);
listener.success(bitmap, imageView);
memoryCacheUtils.setBitmapToMemory(urlStr, bitmap);
saveBitMapToDisk(urlStr, bitmap);
} catch (IOException e) {
listener.failed();
}
}
}
/**
* 将URL转换为MD5
* @param str URL
* @return MD5
*/
private static String calcMD5(String str) {
if (str == null || str.length() == 0) {
throw new IllegalArgumentException("String cannot be null or zero length");
}
StringBuilder hexString = new StringBuilder();
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[] hash = md.digest();
for (byte b : hash) {
if ((0xff & b) < 0x10) {
hexString.append("0").append(Integer.toHexString((0xFF & b)));
} else {
hexString.append(Integer.toHexString(0xFF & b));
}
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hexString.toString();
}
/**
* 计算Bitmap大小,如果超过64kb,则进行压缩
* @param bitmap 源BitMap {@link Bitmap}
* @return 压缩后的Bitmap {@link Bitmap}
*/
private static Bitmap imageCompressL(Bitmap bitmap) {
double targetWidth = Math.sqrt(64.00 * 1000);
if (bitmap.getWidth() > targetWidth || bitmap.getHeight() > targetWidth) {
// 创建操作图片用的matrix对象
Matrix matrix = new Matrix();
// 计算宽高缩放率
float x = (float) Math.max(targetWidth / bitmap.getWidth(), targetWidth / bitmap.getHeight());
// 缩放图片动作
matrix.postScale(x, x);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
}
return bitmap;
}
/**
* LRUCache操作工具类
*/
private static class MemoryCacheUtils {
private static final String TAG = "MemoryCacheUtils";
private final LruCache<String, Bitmap> mMemoryCache;
public MemoryCacheUtils(){
// 得到手机最大允许内存的1/8,即超过指定内存,则开始回收
long maxMemory = Runtime.getRuntime().maxMemory()/8;
Log.i(TAG, "MemoryCacheUtils: maxMemory = " + maxMemory);
// 需要传入允许的内存最大值,虚拟机默认内存24M
mMemoryCache = new LruCache<String,Bitmap>((int) maxMemory){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
/**
* 从内存中读图片
* @param url 图片URL
* @return {@link Bitmap}
*/
public Bitmap getBitmapFromMemory(String url) {
return mMemoryCache.get(url);
}
/**
* 往内存中写图片
* @param url 图片URL
* @param bitmap BitMap
*/
public void setBitmapToMemory(String url, Bitmap bitmap) {
mMemoryCache.put(url,bitmap);
}
}
}
LRUCache本身就是基于LinkHashMap实现的,关于更多LRUCache的内容可以看我的另一篇博客:《LinkHashMap与LRU》
需要注意的是使用此组件需要外部存储的权限与网络访问权限(如果是Android6.0之后的设备还需要动态权限申请):
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />