自定义图片加载器
前言
图片加载器是一个非常常用的功能模块,但是一般我们不会去从零开始自己写一个,因为有Glide、Picasso、Fresco等一些优秀的开源库,或者公司自己维护了一套。再者完整实现这一整套功能是很耗费精力的。而这篇文章仅仅是为了学习程序的设计模式和LruCache、Bitmap优化显示等,去实现这样的一个库。
先说下面向对象六大原则及在此项目里的体现:
1. 单一原则
简单来说,一个类中,应该是一组相关性很高的函数、数据的封装。具体根据类的职责。
2. 开闭原则
提取共同的函数为一个接口,通过设置接口对象或者实现该接口的对象,实现不同的缓存策略。
3. 里氏替换原则
在这里体现的就是只要实现IImageCache接口的都可以设置到缓存去。
4. 依赖倒置原则
模块间依赖通过抽象发生,实现类不直接发生依赖关系,其依赖是通过抽象类和接口。通过这样减少耦合。
5. 接口隔离原则
fileOutputStream等其他流的关闭,这里只要是实现Closeable接口的都可以关闭close();只要知道类实现了closeable,就可关闭,其他的一概不关心,这就是接口隔离。
6. 迪米特原则
ImageCache里面使用了DiskLruCache,但是用户不需要知道实现细节,只需要和ImageCache打交道。即使里面的DiskLruCache替换为其他的缓存实现,用户也不会感知到。
下面开始分析
ImageLoader:负责下载图片和加载的类.
ImageCache:含有LruCache和DiskLruCache.
ImageResizer:获取特定采样的bitmap.
IImageCacahe:抽象ImageCache的接口,之前说的六大原则,很重要的一点是强调抽象.
ImageCache里面使用了LruCache和DiskLruCache:
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
try {
diskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
这里缓存大小cacheSize设置为当前进程可用内存的0.25倍.sizeOf完成对Bitmap对象大小的计算。
lruCache缓存和读取:
lruCache.put(key, bitmap), Bitmap bitmap = lruCache.get(key);
DiskLruCache缓存和读取:
public void addToDiskCache(Bitmap bitmap, String key) throws IOException {
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (addToDisk(bitmap, outputStream)) {
editor.commit();
} else {
editor.abort();
}
diskLruCache.flush();
outputStream.close();
}
}
try {
DiskLruCache.Snapshot snapShot = diskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, width, height);
if (bitmap != null)
addToCache(bitmap, url);
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "getDiskLruCache error");
}
BitmapFactory.decodeStream(in),不能decode两次。所以用ImageResizer.decodeSampledBitmapFromFileDescriptor根据ImageView大小获取对应采样率的Bitmap(因为ImageView小,而图片的分辨率大时,加载原图是很浪费内存的)。主要是计算options.inSampleSize:
public Bitmap decodeSampledBitmapFromFileDescriptor(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);
}
ImageLoader里面先从ImageCache中读取,若是没有,者从网络下载,但是第一次下载获取的Bitmap不能直接设置到ImageView中去。
private IImageCacahe cache;
private Bitmap downloadBitmapFromUrl(String urlString, int width, int height) {
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);
if (cache instanceof ImageCache)
((ImageCache) cache).addToDiskCache(bitmap, Utils.keyFormUrl(urlString));
bitmap.recycle();
bitmap = cache.getFromCache(urlString, width, height);// TODO: 2017/4/4 这里因为第一次下载的原图需要重新采样获取需求大小的bitmap,而BitmapFactory.decodeStream(in),不能decode两次。
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
Utils.close(in);
}
return bitmap;
}
根据imageview的大小,通过BitmapFactory.Options options,,options.inSampleSize,计算需要的inSampleSize压缩得到bitmap再加载到imageview上去。
若未采样优化,加载一屏6张图片原图,总20张,上下滑动时,内存占用情况如下:
优化后内存占用情况如下 :这里只是指定了下需要的bitmap显示的大小(单位为像素,不同的设备上相同的dp显示的像素不一致,所以大小也会不同)。根据需要的大小及其FileDescriptor,从其DiskLruCache获取bitmap,再显示到ImageVIew中去。
对比内存占用区别相当大,但是显示的效果一致并没有打折扣。
还有需要解决的一个问题是ImageView的复用,例如在Listview中加载图片,如果需要加载的图片用户已经划过去了,那么应该忽略这张图片,具体的就是在 target.setTag(TAG_KEY_URI, url),线程池的任务中 handler.obtainMessage(SUCCESS_COMPLETE, new LoaderResult(target, url, bitmap)).sendToTarget();然后在handler的handleMessage中
imageView.getTag(TAG_KEY_URI);对比url是否一致,不一致则忽略该图片。
最后顺便接入了leakcanary,发现内存泄漏:华为mate8上Android6.0的HwPhoneWindow的mContext持有MainActivity对象,导致泄漏;红米note2上Android5.0:未发现有内存泄漏。这里是华为的系统问题,不知有没有大神能指点这该如何解决,感激不尽。
项目源码地址:https://github.com/Ulez/UImageLoader
参考资料:
1.Android开发艺术探索
2.Android源码设计模式解析与实战