Android Image开源框架之ImageLoader(二)

来源:互联网 发布:淘宝黑莓手机是真的吗 编辑:程序博客网 时间:2024/06/04 08:52

一.特点

可配置度高。支持任务线程池、下载器、解码器、内存及磁盘缓存、显示选项等等的配置。包含内存缓存和磁盘缓存两级缓存。支持多线程,支持异步和同步加载。支持多种缓存算法、下载进度监听、ListView 图片错乱解决等。

1.1.总体设计图
这里写图片描述

这里写图片描述

上面是 UIL 的总体设计图。整个库分为 ImageLoaderEngineCacheImageDownloaderImageDecoderBitmapDisplayerBitmapProcessor 五大模块,其中 Cache 分为 MemoryCacheDiskCache 两部分。

简单的讲就是 ImageLoader 收到加载及显示图片的任务,并将它交给 ImageLoaderEngineImageLoaderEngine 分发任务到具体线程池去执行,任务通过 CacheImageDownloader 获取图片,中间可能经过 BitmapProcessorImageDecoder 处理,最终转换为 Bitmap 交给 BitmapDisplayerImageAware 中显示。

1.2. UIL 中的概念

ImageLoaderEngine:任务分发器,负责分发 LoadAndDisplayImageTaskProcessAndDisplayImageTask 给具体的线程池去执行,本文中也称其为 engine。

ImageAware:显示图片的对象,可以是 ImageView 等。

ImageDownloader:图片下载器,负责从图片的各个来源获取输入流 。

Cache:图片缓存,分为 MemoryCache 和 DiskCache 两部分。

MemoryCache:内存图片缓存,可向内存缓存缓存图片或从内存缓存读取图片 。

DiskCache:本地图片缓存,可向本地磁盘缓存保存图片或从本地磁盘读取图片。

ImageDecoder:图片解码器,负责将图片输入流 InputStream 转换为 Bitmap 对象。

BitmapProcessor:图片处理器,负责从缓存读取或写入前对图片进行处理 。

BitmapDisplayer:将 Bitmap 对象显示在相应的控件 ImageAware 上。

LoadAndDisplayImageTask:用于加载并显示图片的任务。

ProcessAndDisplayImageTask:用于处理并显示图片的任务。

DisplayBitmapTask:用于显示图片的任务。

2.主要函数

(1). getInstance()

得到 ImageLoader 的单例。通过双层是否为 null 判断提高性能。

(2). init(ImageLoaderConfiguration configuration)

初始化配置参数,参数 configurationImageLoader 的配置信息,包括图片最大尺寸、任务线程池、磁盘缓存、下载器、解码器等等。

实现中会初始化 ImageLoaderEngine engine 属性,该属性为任务分发器。

(3). displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

加载并显示图片或加载并执行回调接口。 ImageLoader 加载图片主要分为三类接口:

displayImage(…) 表示异步加载并显示图片到对应的 ImageAware 上。
loadImage(…) 表示异步加载图片并执行回调接口。
loadImageSync(…) 表示同步加载图片。
以上三类接口最终都会调用到这个函数进行图片加载。函数参数解释如下:

uri:图片的 uri。uri 支持多种来源的图片,包括 http、https、file、content、assets、drawable 及自定义,具体介绍可见 ImageDownloader 。

imageAware:一个接口,表示需要加载图片的对象,可包装 View。

options:图片显示的配置项。比如加载前、加载中、加载失败应该显示的占位图片,图片是否需要在磁盘缓存,是否需要在内存缓存等。

listener:图片加载各种时刻的回调接口,包括开始加载、加载失败、加载成功、取消加载四个时刻的回调函数。

progressListener:图片加载进度的回调接口。

3.主要属性:

(1). Resources resources

程序本地资源访问器,用于加载 DisplayImageOptions 中设置的一些 App 中图片资源。

(2). int maxImageWidthForMemoryCache

内存缓存的图片最大宽度。

(3). int maxImageHeightForMemoryCache

内存缓存的图片最大高度。

(4). int maxImageWidthForDiskCache

磁盘缓存的图片最大宽度。

(5). int maxImageHeightForDiskCache

磁盘缓存的图片最大高度。

(6)

. BitmapProcessor processorForDiskCache

图片处理器,用于处理从磁盘缓存中读取到的图片。

(7). Executor taskExecutor

ImageLoaderEngine 中用于执行从源获取图片任务的 Executor。

(18). Executor taskExecutorForCachedImages

ImageLoaderEngine 中用于执行从缓存获取图片任务的 Executor。

(19). boolean customExecutor

用户是否自定义了上面的 taskExecutor。

(20). boolean customExecutorForCachedImages

用户是否自定义了上面的 taskExecutorForCachedImages

(21). int threadPoolSize

上面两个默认线程池的核心池大小,即最大并发数。

(22). int threadPriority

上面两个默认线程池的线程优先级。

(23). QueueProcessingType tasksProcessingType

上面两个默认线程池的线程队列类型。目前只有 FIFO, LIFO 两种可供选择。

(24). MemoryCache memoryCache

图片内存缓存。

(25). DiskCache diskCache

图片磁盘缓存,一般放在 SD 卡。

(26). ImageDownloader downloader

图片下载器。

(27). ImageDecoder decoder

图片解码器,内部可使用我们常用的 BitmapFactory.decode(…) 将图片资源解码成 Bitmap 对象。

(28). DisplayImageOptions defaultDisplayImageOptions

图片显示的配置项。比如加载前、加载中、加载失败应该显示的占位图片,图片是否需要在磁盘缓存,是否需要在内存缓存等。

(29). ImageDownloader networkDeniedDownloader

不允许访问网络的图片下载器。

(30). ImageDownloader slowNetworkDownloader

慢网络情况下的图片下载器。

4.主要函数及含义:

(1). build()

按照配置,生成 ImageLoaderConfiguration。代码如下:

public ImageLoaderConfiguration build() {    initEmptyFieldsWithDefaultValues();    return new ImageLoaderConfiguration(this);}

(2). initEmptyFieldsWithDefaultValues()

初始化值为 null 的属性。若用户没有配置相关项,UIL 会通过调用 DefaultConfigurationFactory 中的函数返回一个默认值当配置。
memoryCache 默认值为 LruMemoryCache 。如果内存缓存不允许缓存一张图片的多个尺寸,则用 FuzzyKeyMemoryCache 做封装,同一个图片新的尺寸会覆盖缓存中该图片老的尺寸。

diskCache 默认值与 diskCacheSizediskCacheFileCount 值有关,如果他们有一个大于 0,则默认为 LruDiskCache ,否则使用无大小限制的 UnlimitedDiskCache

downloader 默认值为 BaseImageDownloader

decoder 默认值为 BaseImageDecoder

(3). denyCacheImageMultipleSizesInMemory()

设置内存缓存不允许缓存一张图片的多个尺寸,默认允许。

后面会讲到 View 的 getWidth() 在初始化前后的不同值与这个设置的关系。

(4). diskCacheSize(int maxCacheSize)

设置磁盘缓存的最大字节数,如果大于 0 或者下面的 maxFileCount 大于 0,默认的 DiskCache 会用 LruDiskCache ,否则使用无大小限制的 UnlimitedDiskCache

(5). diskCacheFileCount(int maxFileCount)

设置磁盘缓存文件夹下最大文件数,如果大于 0 或者上面的 maxCacheSize 大于 0,默认的 DiskCache 会用 LruDiskCache ,否则使用无大小限制的 UnlimitedDiskCache

5.杂谈

聊聊 LRU

UIL 的内存缓存默认使用了 LRU 算法。 LRU: Least Recently Used 近期最少使用算法, 选用了基于链表结构的 LinkedHashMap 作为存储结构。

假设情景:内存缓存设置的阈值只够存储两个 bitmap 对象,当 put 第三个 bitmap 对象时,将近期最少使用的 bitmap 对象移除。

图1: 初始化 LinkedHashMap, 并按使用顺序来排序, accessOrder = true;

图2: 向缓存池中放入 bitmap1 和 bitmap2 两个对象。

图3: 继续放入第三个 bitmap3,根据假设情景,将会超过设定缓存池阈值。

图4: 释放对 bitmap1 对象的引用。

图5: bitmap1 对象被 GC 回收。

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

二.源码分析

ImageView mImageView = (ImageView) findViewById(R.id.image);            String imageUrl = "http://a3.topitme.com/a/36/4d/11053913790554d36ao.jpg";            //显示图片的配置            DisplayImageOptions options = new DisplayImageOptions.Builder()                    .showImageOnLoading(R.drawable.ic_stub)                    .showImageOnFail(R.drawable.ic_error)                    .cacheInMemory(true)                    .cacheOnDisk(true)                    .bitmapConfig(Bitmap.Config.RGB_565)                    .build();    ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);  

大部分的时候我们都是使用上面的代码去加载图片

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {          displayImage(uri, new ImageViewAware(imageView), options, null, null);      }  

从上面的代码中,我们可以看出,它会将ImageView转换成ImageViewAware, ImageViewAware主要是做什么的呢?该类主要是将ImageView进行一个包装,将ImageView的强引用变成弱引用,当内存不足的时候,可以更好的回收ImageView对象,还有就是获取ImageView的宽度和高度。这使得我们可以根据ImageView的宽高去对图片进行一个裁剪,减少内存的使用。

我们知道在使用ImageLoader之前,必须进行配置,那么我们就从ImageLoaderConfiguration这个类入手,该类属性如下:

public final class ImageLoaderConfiguration {    final Resources resources;//用来加载drawable图片    //内存缓存最大宽高,默认为屏幕尺寸    final int maxImageWidthForMemoryCache;    final int maxImageHeightForMemoryCache;    //磁盘缓存最大宽高,默认为0,不做限制    final int maxImageWidthForDiskCache;    final int maxImageHeightForDiskCache;    //Bitmap处理器,用来处理原始bitmap,返回一个新bitmap    final BitmapProcessor processorForDiskCache;    final Executor taskExecutor;//线程池,默认3个线程    final Executor taskExecutorForCachedImages;//缓存图片线程池,默认3个线程    //是否使用了自定义的线程池    final boolean customExecutor;    final boolean customExecutorForCachedImages;    //线程池数量、优先级、排队类型(FIFO,LIFO)    final int threadPoolSize;    final int threadPriority;    final QueueProcessingType tasksProcessingType;    final MemoryCache memoryCache;//接口,内存缓存    final DiskCache diskCache;//接口,磁盘缓存    final ImageDownloader downloader;//图片下载器,根据url下载成流    final ImageDecoder decoder;//图片解码器,用于将流解码成bitmap    final DisplayImageOptions defaultDisplayImageOptions;//显示配置    final ImageDownloader networkDeniedDownloader;//禁止网络的下载器(只从本地图片加载图片,可以用来做只在wifi下加载图片这个功能)    final ImageDownloader slowNetworkDownloader;//慢网络的加载器

我们知道构建者模式,需要使用build()来初始化,那么build()又做了什么?

public ImageLoaderConfiguration build() {            initEmptyFieldsWithDefaultValues();//初始化部分空值            return new ImageLoaderConfiguration(this);//赋值        }

可以看出,build()会对一些空值进行初始化,然后在通过ImageLoaderConfiguration的构造方法来赋值参数。ImageLoaderConfiguration的构造方法只是简单的一些赋值操作,我们就不进去看了。现在来看看initEmptyFieldsWithDefaultValues方法。

private void initEmptyFieldsWithDefaultValues() {            if (taskExecutor == null) {//初始化下载线程池                taskExecutor = DefaultConfigurationFactory                        .createExecutor(threadPoolSize, threadPriority, tasksProcessingType);            } else {                customExecutor = true;            }            if (taskExecutorForCachedImages == null) {//初始化缓存线程池                taskExecutorForCachedImages = DefaultConfigurationFactory                        .createExecutor(threadPoolSize, threadPriority, tasksProcessingType);            } else {                customExecutorForCachedImages = true;            }            if (diskCache == null) {//创建磁盘缓存                if (diskCacheFileNameGenerator == null) {                    diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();                }                diskCache = DefaultConfigurationFactory                        .createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);            }            if (memoryCache == null) {//创建内存缓存                memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);            }            if (denyCacheImageMultipleSizesInMemory) {//创建单尺寸内存缓存(同一张图片只缓存一种尺寸到内存中)                memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator());            }            if (downloader == null) {//创建下载器                downloader = DefaultConfigurationFactory.createImageDownloader(context);            }            if (decoder == null) {//创建解码器                decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);            }            if (defaultDisplayImageOptions == null) {//创建默认的显示配置                defaultDisplayImageOptions = DisplayImageOptions.createSimple();            }        }    }

初始化线程池(taskExecutor,taskExecutorForCachedImages

根据队列排队策略,采用了不同的阻塞队列来初始化线程池。此外,可以看出核心线程数和最大线程数是一样的,在ImageLoader中默认开启3个线程。

/** Creates default implementation of task executor */    public static Executor createExecutor(int threadPoolSize, int threadPriority,            QueueProcessingType tasksProcessingType) {        //队列类型        boolean lifo = tasksProcessingType == QueueProcessingType.LIFO;        //队列        BlockingQueue<runnable> taskQueue =                lifo ? new LIFOLinkedBlockingDeque<runnable>() : new LinkedBlockingQueue<runnable>();        //线程池        return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS, taskQueue,                createThreadFactory(threadPriority, "uil-pool-"));    }

初始化缓存(diskCache,memoryCache

先来看下磁盘缓存,createReserveDiskCacheDir可以看出根据是否设置了磁盘缓存大小用了不同的DiskCache。当设置了缓存大小时采用LruDiskCache,LruDiskCache会单独新建一个名为uil-images的目录用来存放,UnlimitedDiskCache用于不限制缓存大小的情况,直接缓存在根目录下(当根目录不可用时,才会选择独立目录)。

public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,            long diskCacheSize, int diskCacheFileCount) {        File reserveCacheDir = createReserveDiskCacheDir(context);//创建独立缓存目录        if (diskCacheSize > 0 || diskCacheFileCount > 0) {            //使用独立的缓存目录            File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);            try {                //如果定义了磁盘缓存大小,则返回一个LruDiskCache                return new LruDiskCache(individualCacheDir, reserveCacheDir, diskCacheFileNameGenerator, diskCacheSize,                        diskCacheFileCount);            } catch (IOException e) {                L.e(e);                // continue and create unlimited cache            }        }        //获取缓存根目录        File cacheDir = StorageUtils.getCacheDirectory(context);        //如果没有定义磁盘缓存大小,则返回一个UnlimitedDiskCache。将根目录和独立目录都传入        return new UnlimitedDiskCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);    }

LruDiskCache内部使用了DiskLruCacheDiskLruCache是JakeWharton开源的一个缓存库,关于DiskLruCache的使用请自行查阅资料,这里只需知道LruDiskCache中使用了DiskLruCache来进行磁盘缓存。UnlimitedDiskCache这个缓存类不用考虑磁盘缓存大小,这里也不做介绍了。此外,ImageLoader中还提供了一个LimitedAgeDiskCache可以指定缓存时间。
关于内存缓存比较简单,如果可以多尺寸缓存使用了LruMemoryCache,否则使用FuzzyKeyMemoryCache。内存缓存都是使用LruCache实现的。这里不做深究。

初始化下载器(ImageDownloader

我们知道下载器是用来根据url来下载为InputStream。那么具体是怎么实现的呢?

public static ImageDownloader createImageDownloader(Context context) {        return new BaseImageDownloader(context);    }

内部返回了BaseImageDownloader,BaseImageDownloader的核心源码如下:

 @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);        }    }

可以看出,根据不同类型使用了不同方法,看到这相信你已经明白该库是怎么支持Drawable等其他类型的了,如果你需要支持自定义的类型,只需要重写getStreamFromOtherSource即可。我们来看看其中两种类型。

初始化解码器(ImageDecoder

DefaultConfigurationFactory.createImageDecoder(writeLogs)内部同样返回了一个BaseImageDecoder,解码器用来将InputStream解码成Bitmap,我们来看看内部的核心源码。

 @Override    public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {        Bitmap decodedBitmap;        ImageFileInfo imageInfo;//保存了图片的大小和旋转信息        InputStream imageStream = getImageStream(decodingInfo);//获取输入流        //..        //省略了部分源码         imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);//从输入流中获取大小信息和旋转信息保存起来,采用了inJustDecodeBounds        imageStream = resetStream(imageStream, decodingInfo);//由于流不能二次读取,所有这里进行重置        //根据获取到的大小,生成一个BitmapFactory.Options        Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);        //根据BitmapFactory.Options来解码bitmap        decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);        //..        //省略了部分源码        if (decodedBitmap == null) {            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());        } else {            //如果bitmap不为空,现在对bitmap进行旋转和翻转操作(如果需要考虑旋转因素)            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,                    imageInfo.exif.flipHorizontal);        }        return decodedBitmap;    }

整个解码流程是这样的,首先从ImageDecodingInfo中获取输入流(ImageDecodingInfo内部保存了下载器,通过下载器下载成流),然后采用inJustDecodeBounds来读取宽高和Exif信息。不同于BitmapFactory.decodeFileInputStream不能二次读取,必须重置,读取到宽高信息后,通过prepareDecodingOptions来计算采样率,然后解码返回bitmap,最后对bitmap处理Exif旋转信息。
ImageDecodingInfo的源码如下:

public class ImageDecodingInfo {    private final String imageKey;    private final String imageUri;    private final String originalImageUri;    private final ImageSize targetSize;    private final ImageScaleType imageScaleType;//图片缩放类型,NONE(不缩放),NONE_SAFE(除非超出硬件加速的显示范围,否则不缩放),IN_SAMPLE_POWER_OF_2(2次幂缩放),IN_SAMPLE_INT(整数缩放),EXACTLY(缩放到至少宽高有一个等于目标值,原始图片小于目标大小则不缩放),EXACTLY_STRETCHED(原始图片小于目标大小仍然缩放)    private final ViewScaleType viewScaleType;//ImageView的缩放类型(被整理成两类,FIT_INSIDE和CROP)    private final ImageDownloader downloader;//图片下载器    private final Object extraForDownloader;//辅助下载器    private final boolean considerExifParams;//考虑旋转参数    private final Options decodingOptions;//解码的BitmapFactory.Options    public ImageDecodingInfo(String imageKey, String imageUri, String originalImageUri, ImageSize targetSize, ViewScaleType viewScaleType,                             ImageDownloader downloader, DisplayImageOptions displayOptions) {        this.imageKey = imageKey;        this.imageUri = imageUri;        this.originalImageUri = originalImageUri;        this.targetSize = targetSize;        this.imageScaleType = displayOptions.getImageScaleType();        this.viewScaleType = viewScaleType;        this.downloader = downloader;        this.extraForDownloader = displayOptions.getExtraForDownloader();        considerExifParams = displayOptions.isConsiderExifParams();        decodingOptions = new Options();        copyOptions(displayOptions.getDecodingOptions(), decodingOptions);    }

初始化显示选项(DisplayImageOptions

在初始化配置中使用了createSimple来创建了一个默认显示选项。

if (defaultDisplayImageOptions == null) {//创建默认的显示配置                defaultDisplayImageOptions = DisplayImageOptions.createSimple();            }

最后

这个框架会不会对本地图片进行磁盘缓存?
从源码可以看出,只要你允许磁盘缓存,任何流到会写入到磁盘内,包括本地图片及Drawable图片。

ImageLoader是怎么实现多尺寸缓存的?那么怎么禁止多尺寸缓存?
多尺寸缓存的核心在于缓存key的格式为[imageUri]_[width]x[height],这样每种尺寸一个key,然后放入内存中。那么ImageLoader怎么禁止多尺寸缓存呢?
很简单,只需配置denyCacheImageMultipleSizesInMemory即可,那么在存放bitmap时会截取url进行遍历比较,如果存在,就移除旧图片。
这里写图片描述
怎么实现仅在wifi环境下加载图片?
很简单,下面一句代码就行。这样在getDownloader()就会返回禁止加载网络图片的下载器。

ImageLoader.getInstance().denyNetworkDownloads(true);

这里写图片描述

0 0
原创粉丝点击