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

转载请注明本文出自文韬_武略的博客(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();
让子类去折腾吧!老子不管了!
那上面说的这些方法什么时候用呢?当然是在添加的时候。
@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

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


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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值