一,写在前面
二,图片缓存
2.1,从内存获取图片缓存,使用LruCache
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
//...
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
//...
public final V put(K key, V value) { //... }
public final V get(K key) { // ... }
protected int sizeOf(K key, V value) {
return 1;
}
//...
}
第1行,LruCache是一个泛型的缓存类;
long maxMemory = Runtime.getRuntime().maxMemory();
int maxBitmapCache = (int) (maxMemory / 1024);
mLruCache = new LruCache<String, Bitmap>(maxBitmapCache){
@Override
protected int sizeOf(String key, Bitmap bitmap) {
int size = bitmap.getHeight() * bitmap.getRowBytes() / 1024;
Log.e("wcc", "size : " + size);
return size;
}
};
LruCache的key是String类型,存放图片的url;value是Bitmap类型,存放BitMap对象。
private Bitmap loadBitmapFromMemoryCache(String url) {
String urlMd5 = makeStrToMd5(url);
return mLruCache.get(urlMd5);
}
代码很简单,调用LruCache$get方法获取Bitmap对象。注意key值并不是url,而是url对应的MD5值。由于url字符串中可能存在非法字符,可以转化为MD5值,一个字符串对应一个唯一的MD5值。
url转化为MD5值
private String makeStrToMd5(String str) {
MessageDigest md;
StringBuilder sb = new StringBuilder();
try {
md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[] byteArray = md.digest();
for(int i = 0; i < byteArray.length; i++) {
int hexInt = byteArray[i] & 0xFF;
String hexString = Integer.toHexString(hexInt);
if (hexString.length() == 1) {
sb.append('0');
}
sb.append(hexString);
}
} catch (Exception e) {
e.printStackTrace();
}
return sb.toString();
}
通过MessageDigest得到一个字节数组byteArray,遍历字节数组,通过与运算byteArray[i] & 0xFF,将字节数转化为十进制的数。然后转化为十六进制的字符串形式,得到hexString。若字符串hexString的值为0~F,则在前面加上0;若字符串hexString的值为10~ff,则不加0。最后将处理后的字符串均放入StringBuilder容器中,return得到url对应的MD5值。
2.2,从磁盘获取图片缓存,使用DiskLruCache
创建DiskLruCache对象
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
private static final long DISK_CACHE_MAX = 50 * 1024 * 1024;
File diskCachePath = new File(context.getCacheDir(), "bitmap");
if (!diskCachePath.exists()) {
diskCachePath.mkdir();
}
try {
mDiskLruCache = DiskLruCache.open(diskCachePath, 1, 1, DISK_CACHE_MAX);
} catch (IOException e) {
e.printStackTrace();
}
private Bitmap loadBitmapFromDiskCache(String url) {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.e("wcc", "do not recommend to load in main thread");
}
Bitmap bitmap = null;
String key = makeStrToMd5(url);
try {
Snapshot snapshot = mDiskLruCache.get(key);
Log.e("wcc", "snapshot null : " + (snapshot == null));
if (snapshot != null) {
FileInputStream in = (FileInputStream) snapshot
.getInputStream(0);
bitmap = decodeCompressBitmapFromStream(in);
}
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
第2行,通过当前线程的Looper检查代码是否在主线程中执行,对这里不理解的可以参考文章
Android中ThreadLocal的工作原理 。
2.2.1,高效加载Bitmap
private Bitmap decodeCompressBitmapFromStream(FileInputStream in) {
Bitmap bitmap = null;
try {
FileDescriptor fd = in.getFD();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, opts);
opts.inJustDecodeBounds = false;
opts.inSampleSize = getInSampleSize(500, 240, opts);
bitmap = BitmapFactory.decodeFileDescriptor(fd, null, opts);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
第4行,将输入流转化为文件描述符FileDescriptor对象,前面已做解释。如果只需要调用一次decodeStream,则不需要转化为FileDescriptor。
private int getInSampleSize(int reqWidth, int reqHeight,
BitmapFactory.Options opts) {
int outWidth = opts.outWidth;
int outHeight = opts.outHeight;
int inSampleSize = 1;
int tempWidth = outWidth / 2;
int tempHeight = outHeight / 2;
while (tempWidth >= reqWidth && tempHeight >= reqHeight) {
tempWidth /= 2;
tempHeight /= 2;
inSampleSize *= 2;
}
return inSampleSize;
}
遵循了两个条件,inSampleSize的值是2的指数,图片宽,高除以inSampleSize后的值>=ImageView的宽,高大小。
2.3,从网络下载图片
2.3.1,将图片存入磁盘,使用DiskLruCache
private boolean addBitmapToDiskCache(InputStream in, String urlPath) {
String key = makeStrToMd5(urlPath);
try {
editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(0);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
第1行,该方法有两个参数,一个是网络下载图片的输入流,一个是图片url。
if (addBitmapToDiskCache(inputStream, urlPath)) {
// 数据流全部写入磁盘
editor.commit();
} else {
// 数据流写入磁盘发生异常,回退操作
editor.abort();
}
mDiskLruCache.flush();
注意在最后一行,调用了DiskLruCache$flush方法,该方法是将内存中相关操作同步到journal文件中。journal文件路径与缓存的图片是同级的目录下的,它记录了缓存图片的存储,查询相关的操作,以及单个缓存图片的大小。因此DiskLruCache要想起作用,最后得调用一次flush方法,将相关操作记录同步到journal文件中。
2.3.2,将Bitmap对象存入内存,使用LruCache
使用LruCache的put方法,key是url的md5值,value是Bitmap对象。
代码实现比较简单 ,方法addBitmapToMemoryCache实现如下:
private void addBitmapToMemoryCache(String url, Bitmap bitmap) {
if (mLruCache != null) {
String key = makeStrToMd5(url);
mLruCache.put(key, bitmap);
}
}
从网络上下载图片,方法loadBitmapFromHttp实现如下:
public Bitmap loadBitmapFromHttp(String urlPath) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException(
"can not access network in main thread");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
try {
URL url = new URL(urlPath);
HttpURLConnection conn = (HttpURLConnection) url
.openConnection();
InputStream inputStream = conn.getInputStream();
// 将图片存入磁盘中
if (addBitmapToDiskCache(inputStream, urlPath)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
bitmap = loadBitmapFromDiskCache(urlPath);
// 将图片存入内存中
addBitmapToMemoryCache(urlPath, bitmap);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
第2行,由于网络加载是一个耗时操作,必然不能在主线程中执行,否则会造成主线程阻塞。
通过主线程里的Looper检查当前代码执行是否在主线程中,不在主线程中执行则抛出异常。
第7行,检查DiskLruCache对象是否为空。
第13~16行,执行网络请求,最终返回一个输入流。
最后,网络下载图片后,将图片缓存存入磁盘和内存中。
小结:图片的磁盘缓存,实际操作的是文件流,存储是输出流,查询是输入流。图片的内存缓存,操作的则是Bitmap对象。
三,另外
上面分别介绍了如何从内存,磁盘,网络获取图片,从磁盘,网络获取图片的操作是一个比较耗时的操作。因此,专门提供一个方法loadBitmapByExecuteTask,封装了从磁盘和网络获取图片,且该方法是在子线程中执行的。
方法loadBitmapByExecuteTask实现如下:
private Bitmap loadBitmapByExecuteTask(String url) {
Bitmap bitmap = null;
bitmap = loadBitmapFromDiskCache(url);
if (bitmap != null) return bitmap;
bitmap = loadBitmapFromHttp(url);
return bitmap;
}
先从磁盘中获取图片,若没有则通过网络加载图片。loadBitmapFromDiskCache和loadBitmapFromHttp方法在前面已经做过详细分析。
四,图片的异步加载
由于很多调用者开一个线程来获取一个图片,因此这里我们需要提供一个方法loadImageToImageView,该方法有两个参数,一个是ImageView对象,一个是图片字符串url,返回值当然是Bitmap对象。这样调用者只需要传入对应控件ImageView的引用,以及图片的url就可以获取图片。
方法loadImageToImageView实现如下:
private void loadImageToImageView(final String url, final ImageView iv) {
Bitmap bitmap = null;
bitmap = loadBitmapFromMemoryCache(url);
if (bitmap != null) {
iv.setImageBitmap(bitmap);
return;
}
Runnable r = new Runnable() {
@Override
public void run() {
Bitmap b = loadBitmapByExecuteTask(url);
LoaderResult mLoaderResult = new LoaderResult(iv, b);
Message msg = mHandler.obtainMessage(MSG_LOAD_IMAGE,
mLoaderResult);
msg.sendToTarget();
}
};
THREAD_POOL_EXECUTOR.execute(r);
}
第3~7行,先从内存中取出图片缓存,若bitmap不为空,调用ImageView$setImageBitmap方法给控件设置图片,最后return。
第20行,THREAD_POOL_EXECUTOR是一个ThreadPoolExecutor类型的变量,它是一个线程池。这里,是直接复制了AsyncTask源码中封装的线程池THREAD_POOL_EXECUTOR的创建过程,对这里不太理解的可以参考文章 Android 源码解析AsyncTask的工作原理 ,关于Android线程池不太了解的可以参考文章关于Android中常用的四种线程池的介绍 ,这里不再重复阐述。
THREAD_POOL_EXECUTOR的创建过程如下:
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 int KEEP_ALIVE = 1;
private static ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader #" + mCount.getAndIncrement());
}
};
private static BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(
128);
private static Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
继续回到方法loadImageToImageView方法:
第9行,创建一个Runnable任务,并作为execute方法的参数传入,run方法执行的任务是在子线程中执行。
第12行,调用loadBitmapByExecuteTask方法获取图片,前面已经有过介绍。
第13行,创建一个类LoaderResult,用于封装ImageView,Bitmap的引用。
第14~16行,通过Handler机制将代码逻辑切换到主线程,并在主线程中操作UI。
类LoaderResult如下:
class LoaderResult {
ImageView iv;
Bitmap bitmap;
public LoaderResult(ImageView iv, Bitmap bitmap) {
this.iv = iv;
this.bitmap = bitmap;
}
}
消息处理如下:
private static final int MSG_LOAD_IMAGE = 0;
private Handler mHandler = new Handler(Looper.getMainLooper()) {
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_LOAD_IMAGE:
LoaderResult result = (LoaderResult) msg.obj;
result.iv.setImageBitmap(result.bitmap);
break;
default:
break;
}
};
};
第7行,调用ImageView$setImageBitmap方法,给控件设置图片。
值得一提的是,在完成异步加载图片的实现时,可以从AsyncTask源码里挖掘一些有用信息。包括线程池THREAD_POOL_EXECUTOR的创建过程,就是直接复制过来的,感觉还比较好用。 在线程间传递数据,创建类LoaderResult封装两个类的引用,然后将 LoaderResult对象作为Message的obj传递,也是从AsyncTask源码学习而来。
五,最后
使用缓存获取图片只需要提供给调用者方法:loadImageToImageView(String url, ImageView iv),调用者并不需要开启一个新的线程,也不需要去实现线程间通信相关的处理。方法loadImageToImageView里封装了线程池,Handler机制,以及图片缓存相关的代码。
另外,对DiskLruCache更详细的分析,见文章 Android DiskLruCache完全解析,硬盘缓存的最佳方案 ,郭霖的这篇文章非常nice,感兴趣的哥们可以参考。
Demo工程地址:ImageLoaderDemo
DiskLruCache源码见工程ImageLoaderDemo 的src目录