这是一个在github上很流行的异步图片加载框架,简单方便实用,主要采用二级缓存来加载缓存图片(不仅仅是网上的,本地也可以),图片加载方式如下。

每次显示图片时,先读取缓存,当缓存没有时,则读取本地数据,只有以上两者都没有的时候才从网络获取。
图片的二级缓存本质也是使用DiskLruCache进行读取,这个东西可以看看郭大神的介绍http://blog.csdn.net/guolin_blog/article/details/28863651,下面来看一下如何使用它。
使用
只需两步,就能轻松实现图片的加载,
- 在Application设置全局参数。
public class UILApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initImageLoader(getApplicationContext());
}
public static void initImageLoader(Context context) {
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
config.threadPriority(Thread.NORM_PRIORITY - )//设置线程的优先权,0-10
.denyCacheImageMultipleSizesInMemory()//设置之后缓存同一URI不同尺寸的图片时,只缓存最后加载的那一张
.diskCacheFileNameGenerator(new Md5FileNameGenerator())//图片的命名方式
.diskCacheSize( * * ) // 磁盘可以保存多大的图片
.tasksProcessingOrder(QueueProcessingType.LIFO)//设置图片处理的顺序为后进先出,默认为FIFO
.writeDebugLogs(); // 输出log
//设置完ImageLoaderConfiguration之后,调用Imageloader的init()方法初始化
ImageLoader.getInstance().init(config.build());
}
}
通过建造者模式设置ImageLoaderConfiguration的属性,然后调用ImageLoader初始化。
-
显示图片
接下来,只需要在调用displayImage方法,来显示图片
//listener 加载周期的监听
//progressListener 加载进度监听
ImageLoader.getInstance().displayImage(String uri, ImageView imageView, DisplayImageOptions options,ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
注意,上面有一个参数DisplayImageOptions ,顾名思义是显示这个图片的参数设置,我们需要自定义它
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub)//加载图片时要显示的图片
.showImageForEmptyUri(R.drawable.ic_empty)//空的url时显示的图片
.showImageOnFail(R.drawable.ic_error)//加载失败时显示的图片
.cacheInMemory(true)//是否缓存
.cacheOnDisk(true)//是否存到磁盘
.considerExifParams(true)//是否考虑exif格式
.displayer(new RoundedBitmapDisplayer())//图片展示的形状
只需要简单的设置以上两步,就能实现高效的图片加载了。点我,进入官方地址供下载demo
源码实现
整个使用过程中除了displayImage,其他的都是参数配置。现在来解析displayImage究竟怎么实现的。
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);
this.configuration = configuration;
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}
在Application 的init初始化中.也就是赋值了全局变量configuration ,和new了一个ImageLoaderEngine。
ImageLoaderEngine也就是一个线程池类,还记得在ImageLoaderConfiguration 中有taskExecutor(Executor executor) 和taskExecutorForCachedImages(Executor executorForCachedImages)方法吗,该类就在这里操作这两个线程池,我们一般不定义那两个方法,使用默认方法构造它们。
//一般使用这个方法,将imageView包装成ImageViewAware
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
displayImage(uri, new ImageViewAware(imageView), options, listener, progressListener);
}
//最终实现的方法
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
//检查ImageLoaderConfiguration 是否为空
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
if (TextUtils.isEmpty(uri)) {
//清除map中key为imageAware的hashcode。它和缓存图片的唯一值组成键值对
engine.cancelDisplayTaskFor(imageAware);
//回调
listener.onLoadingStarted(uri, imageAware.getWrappedView());
//shouldShowImageForEmptyUri在DisplayImageOptions 设置
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}
//一般没有设置目标图片大小,故设置为当前imageview的宽高
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
//根据url和目标图片的宽高组成唯一值
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
//保存到map
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
//回调
listener.onLoadingStarted(uri, imageAware.getWrappedView());
//查看缓存是否有目标图片。
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
//shouldPostProcess()由DisplayImageOptions 设置,自定义一个图片处理器,用来
//下载和解析图片。一般使用系统默认的,他会下载并将图片截到<=imageview长宽。
if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {//同步加载,在主线程执行处理方法,默认不同步
displayTask.run();
} else {//添加到线程池执行
engine.submit(displayTask);
}
} else {
//一般没自定义的情况都执行这里,缓存有,是因为图片经过压缩处理才加载到缓存的,所以不需
//要解析。直接显示
//调用DisplayBitmapTask 的run方法显示图片,
//本质也是调用DisplayImageOptions 设置的display(BitmapDisplay display)显示图片
//框架为BitmapDisplay 接口实现了多个显示形状类
//位于core/display包
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回调
}
} else {//接下来是缓存没有的情况下,
if (options.shouldShowImageOnLoading()) {//DisplayImageOptions 设置,加载时显示图片
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {//DisplayImageOptions设置,加载前置为空
imageAware.setImageDrawable(null);
}
//调用LoadAndDisplayImageTask run()方法,执行图片加载和展示。
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {//同步加载,在主线程执行处理方法,默认不同步
displayTask.run();
} else {//添加到线程池执行
engine.submit(displayTask);
}
}
}
由以上方法可知道,先读取缓存,缓存有直接显示,没有的话到LoadAndDisplayImageTask 执行下一步。
接下来,就来看看LoadAndDisplayImageTask 的run方法,看看是怎么执行的。
public void run() {
//框架为每个url都配备了一个ReentrantLock ,防止同一时间加载同个url多次
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
loadFromUriLock.lock();
Bitmap bmp;
try {
checkTaskNotActual();//检查imageview是否被回收
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
//缓存不存在,加载图片
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
checkTaskInterrupted();
//注意和shouldPostProcess不同,DisplayImageOptions设置
if (options.shouldPreProcess()) {
bmp = options.getPreProcessor().process(bmp);
}
//是否要保存到缓存。取决于DisplayImageOptions 设置,
if (bmp != null && options.isCacheInMemory()) {
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {//缓存有的话,修改个标示而已
loadedFrom = LoadedFrom.MEMORY_CACHE;
}
//DisplayImageOptions设置
if (bmp != null && options.shouldPostProcess()) {
bmp = options.getPostProcessor().process(bmp);
}
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}
//设置要显示的图片信息
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
//调用DisplayBitmapTask 的run方法显示图片,
//本质也是调用DisplayImageOptions 设置的display(BitmapDisplay display)显示图片
//框架为BitmapDisplay 接口实现了多个显示形状类
//位于core/display包
runTask(displayBitmapTask, syncLoading, handler, engine);
}
上面的实现大体上和display()方法差不多,也是拿到图片并且显示,位于的不同是那图片的方法tryLoadBitmap(),看一下是怎么实现的。
//内存没有图片的时候才会调用该方法的。
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
//所以一开始就是去磁盘拿
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > ) {
//草,终于发现,修改标示,给图片url加个file://前缀,代表本地文件
//突然发现,拿到图片像素很大,加载的时候内存哗啦啦的掉
//不行,加载前得缩小它
//decodeImage有加载图片和裁剪图片的功效
loadedFrom = LoadedFrom.DISC_CACHE;
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= || bitmap.getHeight() <= ) {
//草你大爷,还是没有,上网获取,改个标示
loadedFrom = LoadedFrom.NETWORK;
String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();
//解析图片
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null || bitmap.getWidth() <= || bitmap.getHeight() <= ) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
return bitmap;
}
tryLoadBitmap也就是根据url获取到bitmap的实例,想知道怎么获取的,就看看decodeImage是怎么写的,妈的跳来跳去烦死了
private Bitmap decodeImage(String imageUri) throws IOException {
//获取图片缩放方式,这里他把所有缩放方法都归为两类CENTER_INSIDE和CROP
//个人觉得是1等比例缩放到最长边等于imageview的一边
//2等比例缩放到最短边等于imageview的一边,导致会裁剪到最长边的一部分
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
//然后调用框架的解析类,
//这里只有一个解析类位于包core/decode/BaseImageDecoder.java
//ImageLoaderConfiguration会默认设置的
return decoder.decode(decodingInfo);
}
现在,所以的矛头都指向了BaseImageDecoder(默认)类,草蛋的搞完就收工了。
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
//根据url获取输入流,也就是调用core/download/BaseImageDownloader.java
//这个下载类的getStream方法来实现下载,可以去该类看看
InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
return null;
}
try {
//下面这段代码想表达的意思,设置BitmapFactory.Option的inSampleSize
//减少加载时内存的消耗,
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//在这里设置图片的旋转和缩放,inSampleSize不一定缩放到满意的大小
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
在这里需要将一下prepareDecodingOptions方法
protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
//这个ImageScaleType 在DisplayImageOptions里面设置。
//默认是ImageScaleType.IN_SAMPLE_POWER_OF_2这个参数的含义是什么,看下面
ImageScaleType scaleType = decodingInfo.getImageScaleType();
int scale;
if (scaleType == ImageScaleType.NONE) {
scale = ;
} else if (scaleType == ImageScaleType.NONE_SAFE) {
scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
} else {
ImageSize targetSize = decodingInfo.getTargetSize();
//IN_SAMPLE_POWER_OF_2的话powerOf2 设为true
boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
//关于inSampleSize 的计算都在下面的方法中,咬咬牙进去看一下
scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
}
Options decodingOptions = decodingInfo.getDecodingOptions();
decodingOptions.inSampleSize = scale;
return decodingOptions;
}
计算BitmapFactory.Option inSampleSize 的时候有原图片长宽已经目标图片长宽,目标图片时根封装ImageView的ImageViewAmare计算出来的,通过这些信息,计算出符合的图片长宽
public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
boolean powerOf2Scale) {
final int srcWidth = srcSize.getWidth();
final int srcHeight = srcSize.getHeight();
final int targetWidth = targetSize.getWidth();
final int targetHeight = targetSize.getHeight();
int scale = ;
//这个viewScaleType是在ImageViewAware中定义,他将图片的scaletype只是分成两类
switch (viewScaleType) {
//第一类,等比例缩放图片长宽最大值小于等于imageview长宽,这样图片会全部显示,
//imageview不一定被全部占用
case FIT_INSIDE:
//如果设置ImageScaleType.IN_SAMPLE_POWER_OF_2的话,inSampleSize 缩放值为2的平方数
//以5*10的imageview显示100*100的图片为例,如果设置了,scale 为16,没有设置的话,scale 为20
if (powerOf2Scale) {
final int halfWidth = srcWidth / ;
final int halfHeight = srcHeight / ;
while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) {
scale *= ;
}
} else {
scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight); // max
}
break;
//第二类,等比例缩放图片长宽最小值小于等于imageview长宽,这样imageview会全部占用,
//图片可能会被裁剪部分,默认使用这个参数
case CROP:
//以5*10的imageview显示100*100的图片为例,如果设置了,scale 为8,没有设置的话,scale 为10
if (powerOf2Scale) {
final int halfWidth = srcWidth / ;
final int halfHeight = srcHeight / ;
while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) {
scale *= ;
}
} else {
scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight); // min
}
break;
}
if (scale < ) {
scale = ;
}
//根据最大允许长宽值在设置一下scale
scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);
return scale;
}
通过计算得出inSampleSize ,就能大幅度减少图片加载消耗的内存。
注意有ViewScaleType 和 ImageScaleType ,ViewScaleType 是在DisplayImageOptions里面设置。
用来决定decodingOptions.inSampleSize的计算方法,而ImageScaleType 是在ImageViewAware中定义,用来决定scaleType缩放类型,只有等比例缩放和等比例裁剪.
图片的大致加载流程就这样结束了,当然还有很多小细节没有提到,但我已疲软,只能如此了。
总结
-
内存缓存
虽然是自己写的,和LruCache很像,也是使用LinkedHashMap来实现Lru缓存,系统默认使用,如想使用其他缓存方式,可在cache/memory/包查看
-
磁盘缓存
也就是使用DiskLruCache,也是使用LinkedHashMap来实现Lru缓存,系统默认使用,如想使用其他缓存方式,可在cache/memory/包查看
- DisplayImageOption方法介绍
showImageOnLoading(int/drawable) 加载的时候显示的图片
showImageForEmptyUri(int/drawable) url为空时显示的图片
showImageOnFail(int/drawable) 加载失败显示的图片
resetViewBeforeLoading(boolean) 默认为false,当没有设置showImageOnLoading的时候生效,加载图片中的时候imageview图片是否设为空
cacheInMemory(boolean) 默认为false 是否缓存到内存
cacheOnDisk(boolean cacheOnDisk) 默认为false 是否缓存到磁盘
imageScaleType(ImageScaleType) 设置scaletype,与图片的inSampleSize由关系, 默认为 ImageScaleType.IN_SAMPLE_POWER_OF_2,将inSampleSiz设置为2的平方数,一般默认值
bitmapConfig(Bitmap.Config) 图片config默认Bitmap.Config#ARGB_8888
decodingOptions(Options ) 设置图片Option,一般使用imageScaleType配置来动态设置。
delayBeforeLoading(int ) 加载延时
extraForDownloader(Object ) 在ImageDownloader.getStream调用这个参数,但没使用到它,不知何意
considerExifParams(boolean ) 是否设置Exif格式,关于该格式可以百度之
preProcessor(BitmapProcessor) 自定义个图片处理Process,在LoadAndDisplayImageTask 加载完图片之后(下载+设置大小)会调用该方法,修改完之后放到内存中
postProcessor(BitmapProcessor ) 同上,不同的是内存中已经存在,只是拿出来临时修改
displayer(BitmapDisplayer ) 设置imageview显示形状 ,在core/display/包可看类型,默认正常形状加载
syncLoading(boolean ) 加载的时候是否同步,默认异步加载
handler(Handler ) 自定义一个handle来显示图片,默认不需要
- ImageLoaderConfiguration介绍
memoryCacheExtraOptions(int , int ) 设置内存缓存的最大长宽度,默认不处理
diskCacheExtraOptions(int , int ,BitmapProcessor ) 设置磁盘缓存的最大长宽度,默认不处理
taskExecutor(Executor ) 定义线程池存储图片加载线程,默认不处理,取默认值
taskExecutorForCachedImages(Executor ) 定义线程池存储图片加载线程,默认不处理,取默认值
threadPoolSize(int ) 线程池的线程数量,缺省为DEFAULT_THREAD_POOL_SIZE = 3
threadPriority(int ) 线程权限,缺省为 DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY - 2
denyCacheImageMultipleSizesInMemory()同一url图片有不同尺寸时,只缓存最后一个,默认为全部缓存
tasksProcessingOrder(QueueProcessingType )任务处理顺序,有先进先出和后进先出
memoryCacheSize(int ) 内存缓存大小。默认为当前app可用内存的1/8
Builder memoryCacheSizePercentage(int )内存缓存大小占比,1-100
memoryCache(MemoryCache )内存缓存模式,默认为LruCache
diskCacheFileCount(int ) 磁盘缓存文件数量上限
diskCacheFileNameGenerator(FileNameGenerator )文件名生成器,默认为hashcode 生成
imageDownloader(ImageDownloader ) 图片下载器,默认使用缺省值BaseImageDownLoader
imageDecoder(ImageDecoder )图片解析器,默认使用缺省值BaseImageDecoder