打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
图片加载框架Universal-Image-Loader源码解析

今日科技快讯

腾讯表示:在5月10日的某影视公司年度新剧推荐会上,优酷员工用酒杯砸伤了腾讯的女员工。对此,优酷官微回应:你们爱员工,谁不爱员工?但大家都需要真相。人不犯我,我不犯人,江湖事江湖了是条汉子,斗嘴斗气无意义,我们毫无兴趣。

作者简介

明天就是周末啦,提前祝大家周末愉快!

本篇来自 易水南风 的投稿,分享了他对于Universal-Image-Loader框架的分析,希望能对大家有所帮助。

易水南风 的博客地址:

http://blog.csdn.net/sinat_23092639

前言

Universal-Image-Loader

https://github.com/nostra13/Android-Universal-Image-Loader

可以说是安卓知名图片开源框架中最古老、使用率最高的一个了。一张图片的加载对于安卓应用的开发也许是件简单的事,但是如果要同时加载大量的图片,并且图片用于 ListView、GridView、ViewPager 等控件,如何防止出现OOM、如何防止图片错位(因为列表的View复用功能)、如何更快地加载、如何让客户端程序员用最简单的操作完成本来十分复杂的图片加载工作,成了全世界安卓应用开发程序员心头的一大难题,所幸有了Universal-Image-Loader,让这一切变得简单,从某种意义来讲,它延长了安卓开发者的寿命~

针对上述几个问题,Universal-Image-Loader 可谓是兵来将挡水来土掩,见招拆招:

  • 如何同时加载大量图片:采用线程池优化高并发

  • 如何提高加载速度:使用内存、磁盘缓存

  • 如何避免OOM:加载的图片进行压缩,内存缓存具有相关的淘汰算法

  • 如何避免ListView的图片错位:将要加载的图片和要显示的ImageView绑定, 在请求过程中判断该ImageView当前绑定的图片是否是当前请求的图片,不是则停止当前请求。

首先来看下 Universal-Image-Loader 整体流程图: 

总的来说就是:下载图片——将图片缓存在磁盘中——解码图片成为Bitmap——Bitmap的预处理——缓存在Bitmap内存中——Bitmap的后期处理——显示Bitmap

基本用法

关于基本用法想必很多朋友也知道了,或者直接看下Github说明里的使用方法,这里就不再赘述。

和源码见面之前,先和几个重要的类打招呼:

ImageLoaderEngine:任务分发器,负责分发LoadAndDisplayImageTask和ProcessAndDisplayImageTask给具体的线程池去执行。

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

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

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

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

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

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

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

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

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

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

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

其中有个全局图片加载配置类贯穿整个框架,ImageLoaderConfiguration,可以配置的东西实在有点多:

主要是图片最大尺寸、线程池、缓存、下载器、解码器等等。

经过前面那么多的铺垫,终于迎来了源码~~

源码分析

整个框架的源码上万,全部讲完不可能,最好的方式还是按照加载流程走一遍,细枝末节各位可以自己慢慢研究,一旦整体把握好了,其他的一切就水到渠成,切勿只见树木不见森林,迷失在各种代码细节中~~

好了,简单点,讲代码的方式简单点,从最简单的代码切入:

imageLoader.displayImage(imageUri, imageView);

进入方法:

ImageAware 是 ImageView 的包装类,持有 ImageView对象 的弱引用,防止 ImageView 出现内存泄漏发生。主要是提供了获取 ImageView 宽度高度和 ScaleType 等。

最终会执行这一个重载方法:

这个方法基本描绘出了整个图片加载的流程,重要的地方已经加上注释。

if (listener == null)  的 listener 就是上面基本使用说明复杂版本的进度回调接口 ImageLoadingListener,大家看下就知道,如果没有配置的话设置为默认,而默认其实啥都没做,方法都是空实现。

if (options == null) 也是类似地如果没有配置 DisplayImageOptions,即图片显示的配置项,则取默认的。 
看下这个图片显示配置类可以配置什么:

配置是否内存磁盘缓存以及设置图片预处理和后期处理器(预处理和后期处理器默认为 null,留给客户端程序员扩展)等。

displayImage 方法 中 if (targetSize == null) 这一行,如果没有专门设置 targetSize,即指定图片的尺寸,就取 ImageAware 的宽高,即包装在里面的 ImageView 的宽高,如果得到的宽高小于等于0,则设置为最大宽高,如果没有设置内存缓存的最大宽高(maxImageWidthForMemoryCache,maxImageHeightForMemoryCache),则为屏幕的宽高。

然后根据图片uri和图片的尺寸生成一个内存缓存的key,之所以使用图片尺寸是因为内存缓存的图片同一张图片拥有不同尺寸的版本。

下面代码中:

engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

engine 指的是任务分发器 ImageLoaderEngine,它持有一个 HashMap,作为记录图片加载任务使用,一旦任务停止加载或者加载完毕则会删除对应的任务引用。

prepareDisplayTaskFor 方法正是将任务的引用添加到该 HashMap 中,这里以内存缓存的Key为键。

接下来如果拿到的图片需要做后期处理,则创建一个图片显示信息的对象,然后以之前创建的图片显示信息的对象为参数之一创建一个处理显示任务对象 ProcessAndDisplayImageTask(实现Runnable),如果是指定同步加载则直接调用它的run方法,不是则将其添加到任务分发器 ImageLoaderEngine 的线程池中异步执行。

若不需要对图片进行后期处理则调用 Displayer 去显示显示图片,默认的 Displayer 为 SimpleBitmapDisplayer,只是简单地显示图片到指定的 ImageView 中。

如果拿不到内存缓存的对应图片,则创建加载显示图片任务对象 LoadAndDisplayImageTask,然后执行该任务去磁盘缓存或者网络加载图片。

接下来的关键就是看下 LoadAndDisplayImageTask 的 run方法 怎么执行的,就知道整个图片加载怎么运行的了。

run方法不长,就直接展示了,我们看这里:

if (waitIfPaused()) return;
if (delayIfNeed()) return;

第1行中的 waitIfPaused 方法:

这里如果 engine 的 pause 标志位被置位为 true,则会调用 engine.getPauseLock() 对象(其实就是一个普通的Object对象)的wait方法使当前线程暂停运行。为什么要这样呢?看下 engine 的 pause 标志位什么时候会被置位为 true,终于在 PauseOnScrollListener 的重写方法 onScrollStateChanged 中找到:

我们知道 ListView 滑动时候加载显示图片会使得滑动有卡顿现象。这就是在列表滑动时暂停加载的方式,只要给 ListView 设置:

ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling))

就可以通过在滑动时候暂停请求任务从而防止 ListView 滑动带来的卡顿。一旦 ListView 在滑动状态,所有新增的图片请求都会暂停等待列表滑动停止调用 imageLoader.resume()方法 唤醒暂停的线程继续加载请求。

waitIfPaused() 在线程等待过程中被中断(线程池被停止运行)的时候会返回 true,从而终止任务的run方法终止加载请求。它的返回值还由 isTaskNotActual 方法决定,该方法主要判断显示图片的 ImageView 是否被回收以及所在的列表项是否被复用,是的话也是终止加载任务。

delayIfNeed 方法则是在任务需要延迟的时候让当前线程 sleep 一会,同样也是遇到中断返回 true 终止任务。

接下来:

loadFromUriLock.lock();

这里的 loadFromUriLock 是一个 ReentrantLock 对象,是和要加载的图片uri绑定一起的,所以相同uri的图片具有同一把锁,这也就意味着相同图片的请求同一时间只有一个在网络或者磁盘加载,有效避免了重复的网络或者磁盘缓存加载,一旦加载完其余请求都会从内存缓存中加载图片。

然后尝试从内存缓存中取出图片,一旦成功得到图片,则经过请求是否有效的相关判断之后,创建一个 DisplayBitmapTask 将图片显示到对应的 ImageView 中。

关键是如果内存取不到图片的情况,这时候就得看run中:

bmp = tryLoadBitmap();

该方法就是去磁盘缓存取图片,如果没有则取网络图片。方法也不长,所以也直接拷贝过来:

首先从 try 一开始就尝试从磁盘文件系统中寻找对应的图片,如果有的话,调用:

bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));

进行解码得到对应的 Bitmap 对象,注意这里将文件路径使用 Scheme.FILE 进行包装,由于 Universal-Image-Loader 加载图片有多个来源,比如网络,文件系统,项目文件夹 asset 等,所以解码的时候进行一个包装,方便解码器 BaseImageDecoder 在解码的时候识别来源进行相应的获取流处理。

Scheme总共有以下几个:

HTTP('http'), HTTPS('https'), FILE('file'), CONTENT('content'), ASSETS('assets'), DRAWABLE('drawable'), UNKNOWN('')

这里的 decodeImage 方法:

根据 ImageView 的 ScaleType 以及 targetSize 等生成一个 ImageDecodingInfo对象,传入解码器 decoder 的 decode方法 中。

这里的解码器默认为 BaseImageDecoder,decode 方法:

这里具体如何裁剪和旋转的代码就不介绍了,有兴趣大家可以去源码看下。最后得到的图片就可以直接使用显示图片任务 DisplayBitmapTask 将图片显示在 ImageView 上了。

假如磁盘也获取不到图片,那就需要到网络加载了。 tryLoadBitmap 方法中:

if (options.isCacheOnDisk() && tryCacheImageOnDisk())

判断是否需要磁盘缓存,需要的话,进入 tryCacheImageOnDisk 方法:

主要就是通过 downloadImage 方法从网络加载图片,保存到文件系统中。然后判断是否配置了 maxImageWidthForDiskCache 和 maxImageHeightForDiskCache 选项,有的话对保存在文件系统的图片进行裁剪压缩得到缩略图。

然后在 tryLoadBitmap 方法中,取出对应磁盘缓存的图片文件路径,然后使用解码器将图片解码文件为 Bitmap。

以上讨论的是需要磁盘缓存的情况,如果不需要呢?看下 tryLoadBitmap 方法:

bitmap = decodeImage(imageUriForDecoding);

imageUriForDecoding 是图片的uri,该方法上面已经讲过,最终是通过流解码为一个 Bitmap 对象,也就是从网络加载图片到内存,而前面讲的是从文件系统加载图片到内存。

这里需要注意的是,磁盘缓存的是原图或者其缩略图,内存缓存的是根据 ImageView 裁剪的图片。

拿到 Bitmap 对象,然后就是对图片进行预处理了(如果配置为需要的话),处理完毕后添加到内存缓存,然后进行后期处理(如果需要的话),最后将 Bitmap 交给显示任务 DisplayBitmapTask 处理。

DisplayBitmapTask 的 run 就十分简单了:

在确保 ImageView 没有被回收和复用的情况下,交给显示器 displayer 处理,displayer 默认为 SimpleBitmapDisplayer,SimpleBitmapDisplayer 的 display 其实就是直接调用 ImageView 的 setBitmapImage方法 将图片显示上去。然后将该 ImageView 的图片加载任务从任务分发器任务记录中移除。

这里注意下 runTask 的参数 handler,它的来源在 ImageLoader类 的 defineHandler 方法:

可以看到首先取显示配置中的 Handler,默认为 null,所以默认会执行下面的判断,如果调用该方法是在主线程,则创建一个 Handler 对象,于是改 Handler 就和主线程绑定一起了

最后会利用 Handler 的 post方法 将图片的显示传递到主线程调用~~

图片加载过程算是Over了~~

注意要点

1.关于线程池

当加载显示任务要执行的时候,任务分发器的 submit 对应方法是:

其中任务分发线程池 taskDistributor 为缓存线程池(CacheThreadPoll),用于判断任务请求的图片是否缓存在磁盘中以及分发任务给执行任务线程池。两个实际执行任务的线程池 taskExecutorForCachedImages 和 taskExecutor 为可配置的线程池,默认核心和工作线程数都为3,默认配置采用的先进先出的队列,如果是列表图片建议配置为先进后出队列。两个线程池可以根据任务性质自定义配置扩展其他属性。

为什么要专门分为三个线程池而不是一个线程池执行所有任务呢?如果所有任务运行在一个线程池中,所有的任务就都只能采取同一种任务优先级和运行策略。显然果要有更好的性能,在线程数比较多并且线程承担的任务不同的情况下,App中最好还是按任务的类别来划分线程池。

一般来说,任务分为CPU密集型任务,IO密集型任务和混合型任务。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。具体可参见 源代码分析Universal-Image-Loader中的线程池(文有链接末)

2.关于下载器

在加载显示任务类 LoadAndDisplayImageTask 的获取下载器方法中:

默认网络良好的情况下使用 BaseImageDownloader。在无网络情况下使用 NetworkDeniedImageDownloader,当判断到图片源为网络时抛出异常停止请求。网络不佳情况下使用 SlowNetworkDownloader,主要通过重写 FilterInputStream 的 skip(long n) 函数解决在慢网络情况下 decode image 异常的 Bug。

另外关于缓存机制具体可以看下 从源代码分析Android-Universal-Image-Loader的缓存处理机制

总结整个框架的特点:

1.支持内存和磁盘两级缓存。

2.配置高灵活性,包括缓存算法、线程池属性、显示选项等等。

3.支持同步异步加载

4.运用多种设计模式,比如模板方法、装饰者、建造者模式等等,具有很高的可扩展性可复用性。

好了,这个开源框架就讲到这里,希望对大家有帮助,个人水平有限,阅读开源框架源码还不是很多,不足之处请帮忙纠正~~

参考资料

Android 开源框架Universal-Image-Loader完全解析(三)—源代码解读

http://blog.csdn.net/xiaanming/article/details/39057201

Android Universal Image Loader 源码分析

http://a.codekk.com/detail/Android/huxian99/Android%20Universal%20Image%20Loader%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

从源代码分析Universal-Image-Loader中的线程池

http://www.cnblogs.com/kissazi2/p/3966023.html

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Android Universal Image Loader
六款值得推荐的android(安卓)开源框架简介
探索 Glide 原理
高效使用Bitmaps(三) 神奇的Cache
Android 开源框架Universal-Image-Loader完全解析(一)--- 基本介绍及使用
使用ImageLoader实现图片异步加载
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服