其实对于缓存的实现原理及其流程总的来说都很简单,无非就是先从网络加载相关资源,然后用内存缓存或者磁盘缓存把下载到的资源缓存起来;等再次加载相同的资源的时候如果内存缓存或者磁盘缓存还存在就用缓存里面的资源,否则仍然进行网络加载,重复此过程而已。严格说来也没什么可讲的,但是通过研读ImageLoader的源码倒是可以学到很多缓存之外的东西:学学别人的代码怎么设计,资源加载的异步处理机制的灵活使用等等,甚至也可以吸取到一些东西来运用到自己的项目中去。就ImageLoader本身来说,也可以让人了解其工作原理,同时也知道了在配置ImageLoaderConfiguration的时候,那些配置都代表了什么东西,真正达到了知其然知其所以然的目的。
在《ImageLoader的简单分析(二)》这篇博客中主要是对ImageLoader读取内存到最终显示图片的分析。最终的结论是不论是异步还是同步,最终都会调用ImageView的setImageBitmap和setImageDrawable方法。其实你也很容易能想到不论是从内存读取缓存的Bitmap,还是从文件缓存抑或是从网络加载的图片资源,最终都是调用setImageBitmap或者setImageDrawable方法来实现的。尽管理论上很简单,但是本文还是会对读取文件缓存或者从网络资源的实现上针对ImageLoader源码来讲解一遍,看看能从里面学到什么东西。
下面正式开始讲解,当内存缓存中没有对应的bitmap的时候会执行下面一段代码:
//在图片资源加载的过程到最终显示的过程中显示的图片
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {//这个属性还真没用到过
imageAware.setImageDrawable(null);
}
//把下载或者读取文件缓存资源所需的参数组成ImageLoadingInfo对象
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);
}
上面的代码也很简单,主要执行了两步操作:
1)在图片资源从文件缓存或者网络资源读取到最终显示的过程中,显示一个图片,以免用户在此等待期间ImageView什么都不显示而显得界面不友好。当然这需要你创建自己的DisplayImageOptions对象的时候调用showImageOnLoading(Drawable)方法提供一个默认加载中的图片。
2)把图片资源地址,以及ImageLoaderConfiguraton最终封装成LoadAndDisplayImageTask ,顾名思义这个类主要的作用就是加载图片资源并最终使得ImageView显示图片,该类是一个Runnable。如果是同步加载的话就直接当做普通的java对象来执行run方法,否则就交给engine这个ImageLoader内部提供的异步机制对oadAndDisplayImageTask 这个Runnable进行异步执行。
关于ImageLoader的异步处理的方式,会另外开篇博客进行说明。涉及到多线程的东东对我来说也是一个不小的挑战,所以后面会专门列一篇博客进行说明。
下面的分析其实跟上一篇博客一样的顺序进行了,如果觉得啰嗦的话,各位看官可以绕道而行了,闲言少叙,能用代码说明的就不要用语言说明!
LoadAndDisplayImageTask 的run方法取出了异步处理的相关控制之后代码如下:
//先从内存缓存中获取
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
//开始从文件缓存或者网络中加载图片资源
bmp = tryLoadBitmap();
//如果最终获取失败,那么就返回
if (bmp == null) return; //注意此时你自定义的
//在正式使用bitmap之前和放入缓存之前对bitmap进行处理
if (options.shouldPreProcess()) {
bmp = options.getPreProcessor().process(bmp);
}
//如果使用内存缓存的话,就把加载到的bitmap放入缓存
if (bmp != null && options.isCacheInMemory()) {
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {//标志是从内存缓存中读取的资源
loadedFrom = LoadedFrom.MEMORY_CACHE;
}
//如果在加载完成后的图片仍然需要进行处理的话
if (bmp != null && options.shouldPostProcess()) {
bmp = options.getPostProcessor().process(bmp);
}
//创建对象DisplayBitmapTask 最终进行显示。
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
上面的代码逻辑调理也很清晰,从上面可以得到如下的结论:
1)在从文件缓存或者网络获取的图片资源Bitmap在放入缓存之前,如果你的Options对象创建时build了preProcessor(BitmapProcessor),那么那就可以对此Bitmap进行处理,然后把处理过的bitmap放入缓存;
2)如果你的Options允许你对图片进行内存缓存,那么就将(处理过的)bitmap放入memory cache中。
3)如果仍然需要对bmp进行处理,那么就要你为Options配置postProcessor了
4)最终跟读取内存缓存一样,DisplayBitmapTask 进行图片的显示了(详细过程见《ImageLoader简单分析(二)》。
获取你会说,上面的代码没有体现出从文件缓存或者网络资源加载的过程啊?!
马上就开始讲,上面的代码中调用了tryLoadBitmap()方法中做的就是这个活儿!
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
1)从文件缓存中读取uri对应的资源。
File imageFile = this.cache.get(uri);
if (imageFile != null && imageFile.exists()) {//文件缓存存在
//标明是从文件缓存中进行读取的。
loadedFrom = LoadedFrom.DISC_CACHE;
//生成bitmap对象
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {//进入网络加载
//标明是从网络中加载的图片资源
loadedFrom = LoadedFrom.NETWORK;
String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//如果需要文件缓存并且缓存成功
imageFile = this.cache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
//生成bitmap对象
bitmap = decodeImage(imageUriForDecoding);
}
return bitmap;
}
正如你想象的哪样,先从disk cache里面获取bitmap,如果获取失败则从网路中获取图片起源,并且根据是否需要文件缓存来进行来缓存。对于这一点有个地方需要注意的地方(专门把代码提出来):
String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//如果需要文件缓存并且缓存成功
imageFile = this.cache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
//生成bitmap对象
bitmap = decodeImage(imageUriForDecoding);
上面的这段代码主要做了下面几件事儿:
1)首先定义一个imageUriForDecoding 初始值为uri
2)如果DisplayImageOptions允许使用文件缓存,那么就会调用tryCacheImageOnDisk()方法对图片资源进行下载和保存。如果保存成功则修改imageUriForDecoding 的值为Scheme.FILE.wrap(imageFile.getAbsolutePath());
3)如果不允许文件缓存或者说tryCacheImageOnDisk()失败,那么imageUriForDecoding 仍然等于uri.
4)最终调用decodeImage对imageUriForDecoding生成bitmap对象。
在此先不讲解tryCacheImageOnDisk这个方法或者说我们假设不允许文件缓存,那么看看decodeImage解析uri都做了些神马!然后在对tryCacheImageOnDisk方法进行简单讲解。
private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);
}
decodeImage方法会把相关参数配置成ImageDecodingInfo 对象然后交给LoadAndDisplayImageTask的decoder引用所代表的对象进行处理。
decoder是一个ImageDecoder(为接口),初始化代码如下:
public LoadAndDisplayImageTask(ImageLoaderEngine engine, ImageLoadingInfo imageLoadingInfo, Handler handler) {
decoder = configuration.decoder;
}
很明显这个对象的初始化也是从configuration(ImageLoaderConfiguation)里面获取。我们知道ImageLoaderConfiguration的组建是通过其嵌套类Builder来一步步构建的,那么不用说configuration里面的decoder也是有Builder里面组装而来,并且我们在使用ImageLoderConfiguration的时候一般不会配置这个参数,所以Builder必然对其设置了默认值,在上篇博客中就提高Builder类里面有一个方法initEmptyFieldsWithDefaultValues()就是专门提供一些默认值用的,口说无凭,还是让代码说话在initEmptyFieldsWithDefaultValues():方法里面有这么一段代码:
if (decoder == null) {
decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
}
public static ImageDecoder createImageDecoder(boolean loggingEnabled) {
return new BaseImageDecoder(loggingEnabled);
}
从上面的代码我们知道最终我们的decoder为BaseImageDecoder,千呼万唤始出来的感觉!所以马不停蹄的看看他的decode方法吧:
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
//获取一个输入流
InputStream imageStream = getImageStream(decodingInfo);
try {
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
//最终生成decodedBitmap的地方
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
if (decodedBitmap == null) {
decodingInfo.getImageKey());
} else {//对bitmap的在处理
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
这个decoder方法说简单也不简单,里面来回调用的方法也不少,在这里就不多说了(一是偷个懒,而是博主的水平也不是那么6,就不误解读者了)。
那么bitmap的生产过程到此就结束,继续回到之前说过的对tryCacheImageOnDisk()的讲解:这个方法主要是用来从网络中加载数据并把数据保存到文件缓存的。
我屮艸芔茻,不经意间时间过得可真快,一不小心居然凌晨00:40分了!再接再厉,一鼓作气写完再睡觉吧!
先贴出来tryCacheImageOnDisk的代码:
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
boolean loaded;
//下载图片
loaded = downloadImage();
if (loaded) {
int width = configuration.maxImageWidthForDiskCache;//文件缓存的宽度最大值
int height = configuration.maxImageHeightForDiskCache;//文件缓存的高度最大值
if (width > 0 || height > 0) {
//重新设置大小并且进行保存
resizeAndSaveImage(width, height);
}
}
return loaded;
}
从上面的代码可以看出先调用downloadImage()方法对图片资源进行下载:
private boolean downloadImage() throws IOException {
//获取输入流
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
//调用save方法进行保存
return this.cache.save(uri, is, this);
}
上面两个工作也很简单,显示生成输入流,然后调用cache的save方法进行缓存。
因为ImageLoader对生成输入流也不是三言两语能完成的事儿,所以为了不影响这篇博客的结构,对于getStream的讲解还得另外开篇博客,敬请期待。
看看这个save方法都做了些什么,上面代码中的cache的初始化的地方为:
private BaseDiscCache cache;
public LoadAndDisplayImageTask(。。。) {
if(options.isNeedReflection() && (null != options.getReflection())) {
this.cache = options.getReflection();
}else {
this.cache = (BaseDiscCache) configuration.diskCache;
}
}
先分析方法里面吧,根据前面前面所写,既然cache是在configuration里面生成的,那么就还在initEmptyFieldsWithDefaultValues里面里找找这个东西是神马鬼:
if (diskCache == null) {
if (diskCacheFileNameGenerator == null) {
diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
}
diskCache = DefaultConfigurationFactory
.createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
}
最终会调用createDiskCache方法,该方法最终会生成一个UnlimitedDiscCache类的对象,该类是BaseDiscCache的子类,并且没有重写save方法,所以我们只要看BaseDiscCache的save放实现就可以了:
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
//获取文件
File imageFile = getFile(imageUri);
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
boolean loaded = false;
/初始化文件输出流
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
//像缓存文件中写入缓存数据。
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
}
return loaded;
}
到此为止,文件缓存的工作过程算是草草分析完毕! downloadImage()分析完毕后,我们知道ImageLoaderConfiguration是可以对图片缓存文件的最大宽高进行设定的,如果你在builder的过程中对maxImageWidthForDiskCache或者maxImageHeightForDiskCache进行了配置,那么会继续调用resizeAndSaveImage方法来resize后对resize后的bitmap进行保存工作,此时调用的是save的另外一个重载方法:
/** Decodes image file into Bitmap, resize it and save it back */
private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException {
// Decode image file, compress and re-save it
boolean saved = false;
File targetFile = this.cache.get(uri);
if (targetFile != null && targetFile.exists()) {
ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
.....
Bitmap bmp = decoder.decode(decodingInfo);
//如果需要对文件缓存的bitmap进行处理
if (bmp != null && configuration.processorForDiskCache != null) {
bmp = configuration.processorForDiskCache.process(bmp);
}
if (bmp != null) {
saved = this.cache.save(uri, bmp);
bmp.recycle();
}
}
return saved;
}
从前面的讲解中我们知道,在把图片放入memory的时候时候可以通过BitmapProcessor对bitmap进行处理,同样的在上面的代码中也可以看出来,在保存disk cache之前我们也可以调用configuration.processorForDiskCache.process(bmp);对bitmap 进行处理后然后在进行save,该save方法也很简单,下面简单的贴一下代码,不做详细解释:
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
File imageFile = getFile(imageUri);
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
//文件输出流
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
boolean savedSuccessfully = false;
try {
//对bitmap 进行文件缓存
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
}
//回收
bitmap.recycle();
return savedSuccessfully;
}
到此为止,关于ImageLoader读取disk cache或者网络资源的过程分析完毕,简单做个总结如下:
1)如果调用tryLoadBitmap()从文件缓存或者网络中加载图片资源,如果需要把bitmap放入memory cache ,那么在放入memory cache你可以用BitmapProcessor对bitmap处理后再放入缓存!
2)如果disk cache中有存在相关资源使用之,如果不存在那么就开启网络加载。在这里有个需要注意的地方:如果在你的options配置里面不需要文件缓存,那么tryCacheImageOnDisk()方法是不会执行的。此时就跟你平时用ImageView显示网络资源的用法类似,不会走下载保存逻辑!
)tryCacheImageOnDisk()这个方法主要用来对图片资源的下载保存操作。
还是用图说话吧,流程图比较清晰!
其实看懂了这个流程图,那个github官网上提供的下面的图片也就很清楚了:
简单总结这么多吧,本篇篇幅貌似有点长,写的也有点乱,如果有错误的地方欢迎批评指正,共同学习!