universal image loader源码分析——图片内存缓存

来源:互联网 发布:电玩游戏java实战 编辑:程序博客网 时间:2024/06/15 04:22

转载请注明本文出自文韬_武略的博客(http://blog.csdn.net/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();
让子类去折腾吧!老子不管了!
那上面说的这些方法什么时候用呢?当然是在添加的时候。
@Overridepublic 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方法的实现:
@Overridepublic 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类,看它的关键代码:
@Overridepublic 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);}
原来又是一个默认的设置。
@Overridepublic 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

到这里,大致的流程就说玩了。思路其实还是很清晰的吧!
下面是借用下网上绘制的比较好的流程图:


贴张本人对该流程的理解:

鉴于本人水平有限,欢迎大家交流探讨。

0 1