转载请注明本文出自文韬_武略的博客(http://blog.youkuaiyun.com/fwt336/article/details/56004672),请尊重他人的辛勤劳动成果,谢谢!
前言
对于经常使用图片的工程师来说,内存溢出或者卡顿的问题是分成敏感的。而在universal image loader源码中,我们可以看到最常见的几种图片缓存策略,如下图:
下面,我们来一个个分析其中的缓存原理。其中,所有的缓存策略都是线程安全的!
一、内存缓存初始化
对于这经典的图片加载框架,拓展性肯定是还阔以的。用过了imageload的人都知道,Imageload可以配置的参数实在是太多了,所有为了实现内存缓存的易拓展,它使用了builder模式来进行注入内存实例。
在ImageLoaderConfiguration.Builder类下面,我们有看到下面这个方法:
public Builder memoryCache(MemoryCache memoryCache) {
if (memoryCacheSize != 0) {
L.w(WARNING_OVERLAP_MEMORY_CACHE);
}
this.memoryCache = memoryCache;
return this;
}
也就是说,我们在初始化参数的时候,我们就可以注入内存缓存实例了,具体是哪个呢?就看你的项目需求了。
但是好像对于一般的需求我们并没有去设置内存缓存策略吧?那是不是就没用到呢?
private void initEmptyFieldsWithDefaultValues() {
...
if (memoryCache == null) {
memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
}
...
}
在ImageLoaderConfiguration.Builder源码中我们有看到上面的初始化方法,也就是说,当我们没有设置时,有设置默认的缓存策略了,那是哪个呢?
/**
* Creates default implementation of {@link MemoryCache} - {@link LruMemoryCache}<br />
* Default cache size = 1/8 of available app memory.
*/
public static MemoryCache createMemoryCache(Context context, int memoryCacheSize) {
if (memoryCacheSize == 0) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();
if (hasHoneycomb() && isLargeHeap(context)) {
memoryClass = getLargeMemoryClass(am);
}
memoryCacheSize = 1024 * 1024 * memoryClass / 8;
}
return new LruMemoryCache(memoryCacheSize);
}
原来使用的是LruCache缓存策略。
一般,初始化方法很简单:
public static void initImageLoader(Context context) { ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context); config.memoryCache(new LruMemoryCache(40 * 1024 * 1024)); config.threadPriority(Thread.NORM_PRIORITY - 2); config.denyCacheImageMultipleSizesInMemory(); config.diskCacheFileNameGenerator(new Md5FileNameGenerator()); config.diskCacheSize(50 * 1024 * 1024); // 50 MiB config.tasksProcessingOrder(QueueProcessingType.LIFO); config.writeDebugLogs(); // Remove for release app // Initialize ImageLoader with configuration. ImageLoader.getInstance().init(config.build()); }
关于Imageload的内存缓存的使用已经说完了,下面来说下具体的每个缓存策略吧。
二、基类介绍
为了实现易拓展性,接口类和抽象类自然是少不了的。
MemoryCache接口类,列出了外部使用时可能需要使用到的所有接口。
BaseMemoryCache抽象类,则实现了MemoryCache中的所有接口,同时,抽象出了一个方法:
protected abstract Reference<Bitmap> createReference(Bitmap value);
目的是为了拓展图片缓存是强引用呢?还是弱引用呢?
LimitedMemoryCache
还有一个抽象类是LimitedMemoryCache,从它的名字我们便可以看出,它是用于限制图片内存缓存大小的。
通过它的介绍和源码我们可以知道,它同时使用了强引用和弱引用来缓存图片。对于限制范围大小内的图片使用的是强引用
hardCache,而对于其他所有的图片,则使用的是弱引用,这样方便在内存不足时,回收其他的图片,而尽量不去回收正在使用的强引用的图片。
所以,就必定有一个获取最大强引用内存大小的方法:
protected int getSizeLimit() { return sizeLimit; }
和获取当前图片大小的抽象方法了:
protected abstract int getSize(Bitmap value);
当然,当内存超过最大强引用缓存大小后,我们需要移除部分图片缓存来存储新的图片,那是怎么移除呢?是FIFO呢?还是LRU算法呢?还是UsingFreqLimited呢?当然这些都是后面需要说的,所以就还有下面这个方法了:protected abstract Bitmap removeNext();
让子类去折腾吧!老子不管了!那上面说的这些方法什么时候用呢?当然是在添加的时候。
@Override public boolean put(String key, Bitmap value) { boolean putSuccessfully = false; // Try to add value to hard cache int valueSize = getSize(value); int sizeLimit = getSizeLimit(); int curCacheSize = cacheSize.get(); if (valueSize < sizeLimit) { while (curCacheSize + valueSize > sizeLimit) { Bitmap removedValue = removeNext(); if (hardCache.remove(removedValue)) { curCacheSize = cacheSize.addAndGet(-getSize(removedValue)); } } hardCache.add(value); cacheSize.addAndGet(valueSize); putSuccessfully = true; } // Add value to soft cache super.put(key, value); return putSuccessfully; }我们有看到,在将图片存在hardCache前,获取了最大强缓存大小和当前图片的大小,然后进行循环判断是否超出了最大强缓存大小,超出了则移除下一个缓存图片,注意,此时的下一个,不一定是字面上的下一个,而是需要子类去实现具体操作,直到当前强缓存大小可以存储下当前图片为止。So,接下来很多缓存策略都是继承与LimitedMemoryCache的,因为我们很多时候需要限制图片缓存大小。
(为了整片内容不会太过长,所以还是另起一篇吧)
universal image loader源码分析——图片内存缓存策略分析
三、三级缓存的实现
使用过图片加载框架的都知道,图片缓存有三种加载方式,从优先级高到低是:1)从内存加载 ;2)从文件加载;3)网络加载。下面,我们从源码中来看它是怎么来实现的。一般,我们是这么调用它来显示图片的:ImageLoader.getInstance().displayImage(IMAGE_URLS[position], imageView);复杂一点的呢就像这样,定制下option和监听下载:ImageLoader.getInstance().displayImage(IMAGE_URLS[position], imageView, options, new SimpleImageLoadingListener() { @Override public void onLoadingStarted(String imageUri, View view) { spinner.setVisibility(View.VISIBLE); } @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { String message = null; switch (failReason.getType()) { case IO_ERROR: message = "Input/Output error"; break; case DECODING_ERROR: message = "Image can't be decoded"; break; case NETWORK_DENIED: message = "Downloads are denied"; break; case OUT_OF_MEMORY: message = "Out Of Memory error"; break; case UNKNOWN: message = "Unknown error"; break; } Toast.makeText(view.getContext(), message, Toast.LENGTH_SHORT).show(); spinner.setVisibility(View.GONE); } @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { spinner.setVisibility(View.GONE); } });总之,不管你怎么调用 displayImage (***);方法,其实最后都是调用的下面这个方法:public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) { 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)) { engine.cancelDisplayTaskFor(imageAware); listener.onLoadingStarted(uri, imageAware.getWrappedView()); if (options.shouldShowImageForEmptyUri()) { imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources)); } else { imageAware.setImageDrawable(null); } listener.onLoadingComplete(uri, imageAware.getWrappedView(), null); return; } if (targetSize == null) { targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize()); } String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); 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); // 是否需要处理加载进度 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 { // 直接显示 options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); // 回调监听接口 listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); } } else { if (options.shouldShowImageOnLoading()) { imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources)); } else if (options.isResetViewBeforeLoading()) { imageAware.setImageDrawable(null); } 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); // 异步加载 } } }上面加了部分注释可以看下。通过调用上面的方法,我们可以定制图片加载的许多 DisplayImageOptions 参数,如:图片的占位图,加载失败时显示的图片,是否缓存到手机文件中,显示的样式,图片显示动画等等。使用ImageSize还可以设置获取到的图片大小,在显示大图片时由为重要!当然还有两个监听: ImageLoadingListener下载监听和 ImageLoadingProgressListener进度监听。
从displayImage();源码可以看到,优先判断memoryCache内存缓存中是否有缓存该图片?有则直接拿bitmap进行显示,否则再进行处理。上表面的代码看,并没有发现使用diskCache,所以它肯定是将diskCache和网络加载放在一块了。而默认的,使用的是异步加载:if (options.isSyncLoading()) { // 是否同步加载 displayTask.run(); } else { engine.submit(displayTask); // 异步加载 }而真正去加载文件缓存的是LoadAndDisplayImageTask类,这是一个Runnable的实现,所以主要看它的run方法的实现:@Override public void run() { if (waitIfPaused()) return; if (delayIfNeed()) return; ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock; L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey); if (loadFromUriLock.isLocked()) { L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey); } loadFromUriLock.lock(); Bitmap bmp; try { checkTaskNotActual(); bmp = configuration.memoryCache.get(memoryCacheKey); if (bmp == null || bmp.isRecycled()) { bmp = tryLoadBitmap(); if (bmp == null) return; // listener callback already was fired checkTaskNotActual(); checkTaskInterrupted(); if (options.shouldPreProcess()) { L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey); bmp = options.getPreProcessor().process(bmp); if (bmp == null) { L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey); } } if (bmp != null && options.isCacheInMemory()) { L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey); configuration.memoryCache.put(memoryCacheKey, bmp); } } else { loadedFrom = LoadedFrom.MEMORY_CACHE; L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey); } if (bmp != null && options.shouldPostProcess()) { L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey); bmp = options.getPostProcessor().process(bmp); if (bmp == null) { L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey); } } checkTaskNotActual(); checkTaskInterrupted(); } catch (TaskCancelledException e) { fireCancelEvent(); return; } finally { loadFromUriLock.unlock(); } DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom); runTask(displayBitmapTask, syncLoading, handler, engine); }嗯,代码还是有点长的,不过很多都是Log的打印语句和判断语句,主要是判断是否已经加载了该图片,是否需要终止线程,而已还对线程并发做了处理,还加了 ReentrantLock阻塞锁。除开这些,最重要的就是tryLoadBitmap();方法和DisplayBitmapTask加载图片类方法了。如果获取到的bitmap不为空,并且允许缓存到内存中,则缓存到内存缓存中。
tryLoadBitmap()主要代码:Bitmap bitmap = null; File imageFile = configuration.diskCache.get(uri); if (imageFile != null && imageFile.exists() && imageFile.length() > 0) { L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey); loadedFrom = LoadedFrom.DISC_CACHE; checkTaskNotActual(); bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath())); } if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey); 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() <= 0 || bitmap.getHeight() <= 0) { fireFailEvent(FailType.DECODING_ERROR, null); } }
在该方法中我们终于看到了我们的文件缓存:File imageFile = configuration.diskCache.get(uri);当文件缓存中存在该图片缓存时,则拿到这个file的绝对路径:bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));否则,则使用图片的uri远程路径:String imageUriForDecoding = uri; if (options.isCacheOnDisk() && tryCacheImageOnDisk()) { imageFile = configuration.diskCache.get(uri); if (imageFile != null) { imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath()); } }bitmap = decodeImage(imageUriForDecoding);
注意,我们发现,decodeImage();方法有可能被两次调用!第一次是当然是文件缓存中获取,获取失败后,第二次又调用了decodeImage();方法,而这次,就是我们的网络获取了!我们的最终目的是要拿到bitmap,但是是需要调用decodeImage();方法来拿的。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); }而这个方法的主要是通过decoder.decode();方法来生成bitmap。所以我们需要看下这个ImageDecoder类的方法了。通过查看源码发现ImageDecoder是一个接口,那我们使用的是哪个实现类呢?
找啊找啊找,发现在初始化ImageLoadConfiguration时,配置了默认的编码类,当然也可以自定义。if (decoder == null) { decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs); }/** Creates default implementation of {@link ImageDecoder} - {@link BaseImageDecoder} */ public static ImageDecoder createImageDecoder(boolean loggingEnabled) { return new BaseImageDecoder(loggingEnabled); }看到了,是这个 BaseImageDecoder类,看它的关键代码:@Override public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException { Bitmap decodedBitmap; ImageFileInfo imageInfo; InputStream imageStream = getImageStream(decodingInfo); if (imageStream == null) { L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey()); return null; } try { 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 { decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal); } return decodedBitmap; }我们看到它会调用getImageStream();来获取InputStream流,如果为空后就直接返回了null了,那我们拿什么来显示呢?其实,这个地方只有在获取文件缓存的时候才会为null,因为我们后面又调用了个从网络请求图片,走的也是这个方法!然后通过调用 BitmapFactory. decodeStream ();方法来生成bitmap,这个方法我们就很常见了吧?当然获取到的bitmap就是我们真正需要的了,当然也有可能为null,这个时候就真没办法了。而最后面几行代码则是对生成的bitmap进行必要的缩放和旋转处理。所以还有一点没说的是这个getImageStream();protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException { return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader()); }原来它又是一个动态注入,哎,找吧!if (downloader == null) { downloader = DefaultConfigurationFactory.createImageDownloader(context); }/** Creates default implementation of {@link ImageDownloader} - {@link BaseImageDownloader} */ public static ImageDownloader createImageDownloader(Context context) { return new BaseImageDownloader(context); }原来又是一个默认的设置。@Override public InputStream getStream(String imageUri, Object extra) throws IOException { switch (Scheme.ofUri(imageUri)) { case HTTP: case HTTPS: return getStreamFromNetwork(imageUri, extra); case FILE: return getStreamFromFile(imageUri, extra); case CONTENT: return getStreamFromContent(imageUri, extra); case ASSETS: return getStreamFromAssets(imageUri, extra); case DRAWABLE: return getStreamFromDrawable(imageUri, extra); case UNKNOWN: default: return getStreamFromOtherSource(imageUri, extra); } }乖乖,这下都清楚了吧!所有的流来源都是来自这里。具体的每种类型的处理方式就不多少了,大家看下都能懂,其中多了一个判断该uri是否是视频video。
到这里,大致的流程就说玩了。思路其实还是很清晰的吧!下面是借用下网上绘制的比较好的流程图:
贴张本人对该流程的理解:
鉴于本人水平有限,欢迎大家交流探讨。