阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将通过Universal-Image-Loader解析来阐述图片加载框架选型:
一、[Universal-Image-Loader解析基本介绍与使用]
基本介绍
相信大家平时做Android应用的时候,多少会接触到异步加载图片,或者加载大量图片的问题,而加载图片我们常常会遇到许多的问题,比如说图片的错乱,OOM等问题,对于新手来说,这些问题解决起来会比较吃力,所以就有很多的开源图片加载框架应运而生,比较著名的就是Universal-Image-Loader,相信很多朋友都听过或者使用过这个强大的图片加载框架,今天这篇文章就是对这个框架的基本介绍以及使用,主要是帮助那些没有使用过这个框架的朋友们。该项目存在于Github上面Android-Universal-Image-Loader,我们可以先看看这个开源库存在哪些特征
-
多线程下载图片,图片可以来源于网络,文件系统,项目文件夹assets中以及drawable中等
-
支持随意的配置ImageLoader,例如线程池,图片下载器,内存缓存策略,硬盘缓存策略,图片显示选项以及其他的一些配置
-
支持图片的内存缓存,文件系统缓存或者SD卡缓存
-
支持图片下载过程的监听
-
根据控件(ImageView)的大小对Bitmap进行裁剪,减少Bitmap占用过多的内存
-
较好的控制图片的加载过程,例如暂停图片加载,重新开始加载图片,一般使用在 ListView,GridView中,滑动过程中暂停加载图片,停止滑动的时候去加载图片
-
提供在较慢的网络下对图片进行加载
当然上面列举的特性可能不全,要想了解一些其他的特性只能通过我们的使用慢慢去发现了
ImageLoaderConfiguration
图片加载器ImageLoader的配置参数,使用Builder模式。
常用的配置属性有
可以设置内存缓存,硬盘缓存的相关参数等。
设置完相关的参数后就可进行图片加载显示
图片加载
ImageLader提供了几个图片加载的方法,主要是这几个displayImage(), loadImage(),loadImageSync(),loadImageSync()方法是同步的,android4.0有个特性,网络操作不能在主线程,所以loadImageSync()方法我们就不去使用
loadimage()加载图片
我们先使用ImageLoader的loadImage()方法来加载网络图片
传入图片的url和ImageLoaderListener, 在回调方法onLoadingComplete()中将loadedImage设置到ImageView上面就行了,如果你觉得传入ImageLoaderListener太复杂了,我们可以使用SimpleImageLoadingListener类,该类提供了ImageLoaderListener接口方法的空实现,使用的是缺省适配器模式
如果我们要指定图片的大小该怎么办呢,这也好办,初始化一个ImageSize对象,指定图片的宽和高,代码如下
上面只是很简单的使用ImageLoader来加载网络图片,在实际的开发中,我们并不会这么使用,那我们平常会怎么使用呢?我们会用到DisplayImageOptions,他可以配置一些图片显示的选项,比如图片在加载中ImageView显示的图片,是否需要使用内存缓存,是否需要使用文件缓存等等
DisplayImageOptions
可以配置一些图片显示的选项,比如图片在加载中ImageView显示的图片,是否需要使用内存缓存,是否需要使用文件缓存等等,可供我们选择的配置如下
大家就可以根据实际情况去设置。
displayImage()加载图片
接下来我们就来看看网络图片加载的另一个方法displayImage(),代码如下
可以看到这里是直接传递了ImageView进行设置显示,并不需要监听后设置,这样更为简便,这也是displayImage
和loadImage
的区别。
加载其他来源的图片
使用Universal-Image-Loader框架不仅可以加载网络图片,还可以加载sd卡中的图片,Content provider等,使用也很简单,只是将图片的url稍加的改变下就行了,下面是加载文件系统的图片
我们只需要给每个图片来源的地方加上Scheme包裹起来(Content provider除外),然后当做图片的url传递到imageLoader中,Universal-Image-Loader框架会根据不同的Scheme获取到输入流
获取到对应URL后就可以调用display/loadImage方法进行显示。
GirdView,ListView加载图片
相信大部分人都是使用GridView,ListView来显示大量的图片,而当我们快速滑动GridView,ListView,我们希望能停止图片的加载,而在GridView,ListView停止滑动的时候加载当前界面的图片,这个框架当然也提供这个功能,使用起来也很简单,它提供了PauseOnScrollListener这个类来控制ListView,GridView滑动过程中停止去加载图片,该类使用的是代理模式
第一个参数就是我们的图片加载对象ImageLoader,
第二个是控制是否在滑动过程中暂停加载图片,如果需要暂停传true就行了,
第三个参数控制猛的滑动界面的时候图片是否加载
OutOfMemoryError
虽然这个框架有很好的缓存机制,有效的避免了OOM的产生,一般的情况下产生OOM的概率比较小,但是并不能保证OutOfMemoryError永远不发生,这个框架对于OutOfMemoryError做了简单的catch,保证我们的程序遇到OOM而不被crash掉,但是如果我们使用该框架经常发生OOM,我们应该怎么去改善呢?
-
减少线程池中线程的个数,在ImageLoaderConfiguration中的(.threadPoolSize)中配置,推荐配置1-5
-
在DisplayImageOptions选项中配置bitmapConfig为Bitmap.Config.RGB_565,因为默认是ARGB_8888, 使用RGB_565会比使用ARGB_8888少消耗2倍的内存
-
在ImageLoaderConfiguration中配置图片的内存缓存为memoryCache(new WeakMemoryCache()) 或者不使用内存缓存
-
在DisplayImageOptions选项中设置.imageScaleType(ImageScaleType.IN_SAMPLE_INT)或者imageScaleType(ImageScaleType.EXACTLY)
二、[Universal-Image-Loader解析内部缓存原理]
对于我们所知道的缓存,常用的是内存缓存MemoryCache和硬盘缓存DiscCache。一个读取快容量小,一个读取慢容量大。
对于各自使用哪种缓存,则可以在前面配置ImageLoaderConfiguration
进行缓存设置,当然也可以自己自定义适合的缓存。
对于Universal-Image-Loader来说它的缓存结构也是分为内存缓存MemoryCache和硬盘缓存DiskCache
一.MemoryCache内存缓存
首先先看个结构图,理解UIL里面内存缓存的结构
由于空间有限就没画成标准的UML类图形式。
对于基类MemoryCache
它则是一个接口,里面定义了put,get图片的方法
都是大家比较所熟悉的方法,而对于其他的类
我们一个个看
LruMemoryCache
这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用。直接实现了MemoryCache
方法
LruMemoryCache
的源码也比较简单,内部有个成员变量LinkedHashMap<String, Bitmap> map
这里直接进行保存的话则是强引用的形式。
主要看get,put方法。
对于get方法来说,比较简单,直接根据指定的key返回对应的图片。
而对于put方法来说,则需要考虑容量的问题。
put方法首先调用了sizeof
方法,该方法则是返回指定Bitmap的字节大小,之后size +=,总缓存量增加,之后调用trimToSize
该方法则是进行缓存容量判断的。
如果加入后的size 缓存容量 <= maxSize 最大缓存容量,则直接break,不用进行判定处理。
如果大于的话,则直接移除最久未使用的。
大家肯定有疑问,它到底怎么判断最久未使用的?没看到相关代码呀?
相信知道LinkedHashMap
的话可能就知道。LinkedHashMap
自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回false,即不删除数据。大家常见也就是按顺序存储,很少忘了它还可以根据最近未使用的方法。
回看我们前面LinkedHashMap
的创建
再举个使用例子
就比较明了了。
BaseMemoryCache
BaseMemoryCache
同样也是实现了MemoryCache
方法,不过它还是一个抽象类。
它是一个内存缓存的基类,实现了内存缓存中常用的方法,只不过它里面提供了一个非强引用的Reference
作为扩展,方便GC的回收,避免OOM.
代码也比较简单,内存持有一个Map<String, Reference<Bitmap>> softMap
来保存非强引用对象,具体的引用类型则看它实现的抽象方法createReference
。
WeakMemoryCache
我们看它的一个子类WeakMemoryCache
则是继承与BaseMemory
,实现createReference
很明显是来保存弱引用对象的。
LimitedMemoryCache
我们看它的另外一个子类LimitedMemoryCache
,但它并没有实现BaseMemoryCache
里的createReference
方法,它也是一个抽象类,在BaseMemoryCache
基础上封装了个抽象方法protected abstract Bitmap removeNext();
用来处理当缓存容量不足时的情况。
可以看到在 LimitedMemoryCache
里面又有一个List<Bitmap>
保存的是强引用,而在BaseMemoryCache
里面也有个Map<String, Reference<Bitmap>> softMap
来保存Bitmap,为什么要这样。
这主要是因为在BaseMemoryCache
里面并没有做缓存限制处理,它只是封装实现了基本的Bitmap的put,get。而当面对缓存容量有限的情况下,则需要交给子类去处理。
我们看下这里的put方法,关键在
当超过容量时,调用抽象方法removeNext
由子类自行实现,之后hardCache移除,但此时并没有调用softMap的移除。
也就是对于List<Bitmap>
来说,当它的缓存容量超过的时候,它会移除第一个对象来缓解容量,但是保存在Map<String, Reference<Bitmap>> softMap
里面的Bitmap并没有被移除。
如果这样下去softMap岂不是会无限大?
这是因为在Map<String, Reference<Bitmap>> softMap
里面保存的Bitmap是弱引用的存在,而在List<Bitmap>
里面保存的是强引用,当内存不足的时候,GC则会先清除softMap里面的对象。
FIFOLimitedMemoryCache
我们看下LimitedMemoryCache
的一个子类FIFOLimitedMemoryCache
,看到FIFO也就是先进先出了。
可以看到同样的这里也有个List<Bitmap> queue
来保存记录,而在removeNext
那里,返回的正是队列的第一个元素,符合FIFO。
LRULimitedMemoryCache
再来看一个另外一个子类LRULimitedMemoryCache
也就是最近未使用删除。
可以看到,这里的LRU处理则是使用LinkedHashMap
,在它的构造方法中第三个参数为true
表示使用LRU,之后再removeNext
返回那个Bitmap。
同理其他子类也如下,就不一一列举。
MemoryCache小结
1. 只使用的是强引用缓存
- LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用)
2.使用强引用和弱引用相结合的缓存有
-
UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
-
LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)
-
FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
-
LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)
-
LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)
3.只使用弱引用缓存
- WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)
二.DiskCache硬盘缓存
同样先来看个结构
DiskCache的设计其实和MemoryCache一样,对于基类DiskCache
,它同样是一个接口
同样一个个看
LruDiskCache
LruDiskCache
则是直接实现了DiskCache
接口,采用LRU算法来进行缓存处理。
再理解LruDiskCache
前,先理解另一个类DiskLruCache
这个DiskLruCache
比较长也比较复杂,它是LruDiskCache
的一个文件工具类。这里的缓存数据存储在文件系统上的一个目录。
同时也注意到这里的一个成员变量private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
可以知道这是用来处理LRU的。
同时这里的value则是Entry
,Entry
则是封装了当前文件的编辑情况Ediotr
以及key
。
而这里Editor
封装了文件的写入情况OutputStream
,Snapshot
封装了文件的读取情况InputStream
。
回头看回LruDiskCache
首先LruDiskCache
内部成员变量带有DiskLruCache
还有文件的保存目录等,在它的构造方法中调用DiskLruCache.open
方法创建了DiskLruCache
对象,而在它的open方法里,则根据文件的目录情况创建了对应的文件系统。
再看它的save方法,先调用getKey
方法将uri转换为对应的key,而在cache,edit中
则是根据指定的key先判断缓存文件中有没有相应的key,如果没有则创建一个Entry
对象持有它,之后保存在lruEntries
之后,创建一个当前Entry
的编辑对象Editor
,以便之后写入到文件中。
s之后调用了
在editor.newOutputStream
则是根据当前目录和key创建出一个文件,之后打开这个文件的一个输出流情况,获取到之后就进行Bitmap的写入。
同理,看下LruDiskCache
的get方法
调用了cache,get
在get方法中,先根据key拿到对应的Entry
,再拿到对应的文件打开输入流,之后传入到Snapshot
,
而在snapshot.getFile
中
返回的则是对应的文件。
BaseDiskCache
BaseDiskCache
同样也是直接实现了DiskCache
方法,实现的方法也比较简单
比较简单,根据对应的文件去打开获取。它的两个子类LimitedAgeDiskCache
和UnlimitedDiskCache
也都不一一扩展开了。
三、Universal-Image-Loader解析之源代码解析
当我们配置好ImageConfiguration
和ImageLoader
后,我们就会开始调用
这两个方法其中一个来显示图片。
先看loadImage
首先调用了checkConfiguration
用来判断是否有初始化ImageLoaderConfiguration
如果有设置ImageView的大小,则设置,没则默认Configuration的大小。
如果没有设置DisplayImageOptions
,则设置上一个默认的options
之后创建了个NonViewAware
,再调用displayImage
。
也就是说,loadImage
最终还是调用到了displayImage
。
ImageAware
这里的NonViewAware
实现了ImageAware
接口。先来看个结构图
ImageAware
是一个接口,内部提供了一系列操作图片的一些方法。
对于NonViewAware
来说,它内部只是简单的保存图片一些必要的数据,比如图片大小尺寸,URI,ScaleType这些。主要封装成ImageAware
来给displayImage
调用。
看下displayImage
的使用
这里把ImageView封装成ImageViewAware
再去调用displayImage
这个就跟loadImage
一样。
而这里ImageViewAware
继承与ViewAware
,ViewAware
则实现了ImageAware
接口。
与NonViewAware
不同的是ViewAware
内部持有一个Reference<View> viewRef
的成员变量,它是用来保存当前ImageView
的一个弱引用,以便之后来直接设置显示图片。ViewAware
很多方法都是依赖于这个View
之后就可以在ImageViewAware
中设置显示。
好了回过头看他们最终调用的方法。
这个方法有点长,我们拆分成一部分一部分来看
首先先检查是否有初始化设置ImageLoaderConfiguration
没则抛出异常,没设置listener和DisplayImageOptions则设置一个默认值。
之后调用TextUtils.isEmpty(uri)
判断是否当前的uri为空,则调用engine.cancelDisplayTaskFor(imageAware);
之后则用listener通知开始和结束,也比较好理解,主要是这个engine。
这个engine就是ImageLoaderEngine
,主要用来负责显示加载图片的一个类。ImageLoaderEngine
中存在一个HashMap,用来记录正在加载的任务,加载图片的时候会将ImageView的id和图片的url加上尺寸加入到HashMap中,加载完成之后会将其移除。
接着看下面
当URI不为空的时候来加载显示。首先根据uri获取对应uri对应唯一的一个Key,之后调用engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
来记录当前加载的任务,开启listener的start回调,接着调用Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
来获取内存缓存中的图片,这里默认的内存缓存是LruMemoryCache
,前篇文章有分析到。
如果缓存中存在相应的Bitmap的话,进入到if里面
我们如果在DisplayImageOptions中设置了postProcessor就进入true逻辑,不过默认postProcessor是为null的,BitmapProcessor接口主要是对Bitmap进行处理,这个框架并没有给出相对应的实现,如果我们有自己的需求的时候可以自己实现BitmapProcessor接口(比如将图片设置成圆形的).
然后到了27行
将Bitmap设置到ImageView上面,这里我们可以在DisplayImageOptions中配置显示需求displayer,默认使用的是SimpleBitmapDisplayer,直接将Bitmap设置到ImageView上面,我们可以配置其他的显示逻辑, 他这里提供了FadeInBitmapDisplayer(透明度从0-1)RoundedBitmapDisplayer(4个角是圆弧)等, 然后回调到ImageLoadingListener接口。
我们知道loadImage
和displayImage
的区别在于loadImage
依靠返回的Bitmap进行设置显示,而displayImage
则是直接显示。而loadImage
最终也是调用了displayImage
,原因就在于这个display和imageAware
loadImage
的ImageAware
是NonImageAware
并没有处理setImageBitmap
的方法,而displayImage
的ImageViewAware
则有处理显示。
好,继续前面,当从内存缓存获取到的Bitmap为空的情况下
如果需要设置显示加载中的图片,则进行设置显示。ImageLoadingInfo
则是一个加载显示图片任务信息的一个类。
之后根据它创建了一个LoadAndDisplayImageTask
类,它实现了Runnable
。
如果配置了isSyncLoading为true, 直接执行LoadAndDisplayImageTask的run方法,表示同步,默认是false,将LoadAndDisplayImageTask提交给线程池对象
接下来我们就看LoadAndDisplayImageTask的run(), 这个类还是蛮复杂的,我们还是一段一段的分析。
如果waitIfPaused(), delayIfNeed()返回true的话,直接从run()方法中返回了,不执行下面的逻辑, 接下来我们先看看waitIfPaused()
这个方法是干嘛用呢,主要是我们在使用ListView,GridView去加载图片的时候,有时候为了滑动更加的流畅,我们会选择手指在滑动或者猛地一滑动的时候不去加载图片,所以才提出了这么一个方法,那么要怎么用呢? 这里用到了PauseOnScrollListener这个类,使用很简单ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我们缓慢滑动ListView,GridView是否停止加载图片,pauseOnFling 控制猛的滑动ListView,GridView是否停止加载图片。
我们可以看下这个PauseOnScrollListener
的处理
滑动停止的话会调用到imageLoader.pause
所以调用pause.get
则会返回true。
除此之外,这个方法的返回值由isTaskNotActual()决定,我们接着看看isTaskNotActual()的源码
isViewCollected()是判断我们ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,isViewReused()判断该ImageView是否被重用,被重用run()方法也直接返回,为什么要用isViewReused()方法呢?主要是ListView,GridView我们会复用item对象,假如我们先去加载ListView,GridView第一页的图片的时候,第一页图片还没有全部加载完我们就快速的滚动,isViewReused()方法就会避免这些不可见的item去加载图片,而直接加载当前界面的图片。
回头继续看run方法
第4行代码有一个loadFromUriLock,这个是一个锁,获取锁的方法在ImageLoaderEngine类的getLockForUri()方法中
从上面可以看出,这个锁对象与图片的url是相互对应的,为什么要这么做?也行你还有点不理解,不知道大家有没有考虑过一个场景,假如在一个ListView中,某个item正在获取图片的过程中,而此时我们将这个item滚出界面之后又将其滚进来,滚进来之后如果没有加锁,该item又会去加载一次图片,假设在很短的时间内滚动很频繁,那么就会出现多次去网络上面请求图片,所以这里根据图片的Url去对应一个ReentrantLock对象,让具有相同Url的请求就会在第10行等待,等到这次图片加载完成之后,ReentrantLock就被释放,刚刚那些相同Url的请求就会继续执行第10行下面的代码。
之后来到第13行,先调用checkTaskNotActual
判断当前View是否被GC回收使用,是则抛出异常。
接着15行,它们会先从内存缓存中获取一遍,如果内存缓存中没有在去执行下面的逻辑,所以ReentrantLock的作用就是避免这种情况下重复的去从网络上面请求图片。
17行的方法tryLoadBitmap()
,这个方法确实也有点长,我先告诉大家,这里面的逻辑是先从文件缓存中获取有没有Bitmap对象,如果没有在去从网络中获取,然后将bitmap保存在文件系统中,我们还是具体分析下
首先在第4行会去磁盘缓存中去获取图片,如果图片已经保存在磁盘了,则直接获取对应的File路径,调用bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
进行解析。
如果在磁盘中没有的话,则到了12行,开始进行网络下载获取。
在17行会去调用isCacheOnDisk
判断是否要保持在磁盘中,如果默认false,如果是则调用tryCacheImageOnDisk
来下载图片并且保持在磁盘
调用了downloadImage
进行下载图片
可以看到这里调用了getDownloader().getStream
来下载,这里先不扩展,在后面会说到
下载之后则保存在磁盘中。
回来前面
这里有个String变量imageUriForDecoding,初始值是uri,如果有设置磁盘缓存的话,则会调用tryCacheImageOnDisk
来下载并且保持图片,此时的imageUriForDecoding则是文件File的路径。
如果没有设置磁盘缓存的话,则imageUriForDecoding还是uri。
关键则是在decodeImage
,它能根据对应的uri来加载图片。
把传递进来的imageUri(可能是文件的uri,也可能是图片的uri)封装到ImageDecodingInfo
进行解析。
这里的decoder是ImageDecode
,它的默认实现类是BaseImageDecode
通过getImageStream
来获取输入流
这里的Downloader默认实现类是BaseImageDownloader
可以看到,在这里,已经做了多种情况的读取判断。第一篇文章就有介绍到UIL可以根据不同的uri来解析图片,其原理就是在这里。
而前面通过tryCacheImageOnDisk
来下载图片也是根据这个。这里就不一一扩展开。
这里的网络下载图片内部则是使用HttpUrlConnection
来下载的。
回到最前面LoadAndDisplayImageTask
的run方法后面,当我们获取到Bitmap后,到了
这两个代码就是一个显示任务
直接看DisplayBitmapTask类的run()方法
假如ImageView被回收了或者被重用了,回调给ImageLoadingListener接口,否则就调用BitmapDisplayer去显示Bitmap。到这里Bitmap已经显示加载完成,调用engine移除图片显示任务。
当然在最前面那里
如果此时的显示加载是异步的话,则交由engine的Executor
线程池去处理,最终也是调用了LoadAndDisplayImageTask
的run方法去加载显示。
到这里Universal-Image-Loader的分析也算完了,从基本使用到内存模型在加载显示,可以看到UIL这个开源框架十分的灵活,比如建造者模式,装饰模式,代理模式,策略模式等等,这样方便我们去扩展,实现我们想要的功能,当然,也带给我们更多的想象空间。
原文链接:https://www.jianshu.com/p/cff58eddb4ae
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680