概述
Universal-Image-Loader是经典的图片加载框架,虽然现在该项目不再维护,但对于初学者依旧是值得学习的开源项目之一,本文就该框架的加载图片流程做简要梳理,希望读者有所收获。
该文参考了【codeKK】 Android Universal Image Loader 源码分析一文,该文详细分析了Universal-Image-Loader的设计思想,想深入了解,可以祥读此文。
基本工作流程
首先来看看作者给出的工作流程图:

有兴趣的同学可以分析下这些缓存实现,本文只分析
LruMemoryCache
,其实该类个人理解是简化版的LruCache
,看看源码:
/**
* A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to
* the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may
* become eligible for garbage collection.<br />
* <br />
* <b>NOTE:</b> This cache uses only strong references for stored Bitmaps.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.8.1
*
* 缓存了一定数量的Bitmap的强引用。当一个Bitmap被访问时,它会移动到序列的队尾。当缓存满时,再添加Bitmap,会将
* 序列头部的Bitmap释放掉,等待GC回收。
*
*/
public class LruMemoryCache implements MemoryCache {
//内部使用LinkedHashMap来保存Bitmap
private final LinkedHashMap<String, Bitmap> map;
//最大缓存的字节数
private final int maxSize;
/** Size of this cache in bytes */
private int size;//当前缓存的字节数
/** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
public LruMemoryCache(int maxSize) {
if (maxSize <= 0) {//检查设置的缓存大小,小于0抛出异常
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//创建一个基于访问顺序的LinkedHashMap,设置false就是默认的插入顺序,这里不讨论LinkedHashMap的实现了
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}
/**
* Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head
* of the queue. This returns null if a Bitmap is not cached.
* 根据key获取缓存的Bitmap,如果能够获取到,那么该引用会移动到队列尾部。如果没有缓存就返回Null
*/
@Override
public final Bitmap get(String key) {
if (key == null) {//检查key是否为null,是null抛异常
throw new NullPointerException("key == null");
}
//同步,多线程访问时保证线程安全
synchronized (this) {
return map.get(key);
}
}
/** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue.
* put进的bitmap会放进队尾
*/
@Override
public final boolean put(String key, Bitmap value) {
if (key == null || value == null) {//检查key和value值,如果为null抛出异常
throw new NullPointerException("key == null || value == null");
}
synchronized (this) {//开启同步
size += sizeOf(key, value);//累计计算bitmap的大小
Bitmap previous = map.put(key, value);//加入map
if (previous != null) {//如果之前存在该缓存的bitmap,那么size就不再累计该bitmap的大小
size -= sizeOf(key, previous);//
}
}
//检查size是否在合理的范围内,如果不再做相应处理
trimToSize(maxSize);
return true;
}
/**
* Remove the eldest entries until the total of remaining entries is at or below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements.
*
* 该方法主要控制缓存的bitmap不超过maxSize,一旦超过就移除最久没用用过的bitmap,直到小于maxSize
*/
private void trimToSize(int maxSize) {
while (true) {//开启循环
String key;
Bitmap value;
synchronized (this) {//开启同步
if (size < 0 || (map.isEmpty() && size != 0)) {//检查size或map是否正常,否则抛出异常
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
//如果size小于maxSize或者map是空的,那么说明map状态正常,结束循环
if (size <= maxSize || map.isEmpty()) {
break;
}
//查找队列头部存储的bitmap
Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
if (toEvict == null) {//如果要移除的entry对象为null,那么也结束循环
break;
}
//获取key,和value
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);//移除对应的bitmap
size -= sizeOf(key, value);//重新计算size大小
}
}
}
/** Removes the entry for {@code key} if it exists.
* 根据Key移除对应的bitmap
*/
@Override
public final Bitmap remove(String key) {
if (key == null) {//检查key是否为null,null抛出异常
throw new NullPointerException("key == null");
}
synchronized (this) {//开启线程同步
Bitmap previous = map.remove(key);//移除
if (previous != null) {//如果之前的bitmap存在,那么重新计算size大小
size -= sizeOf(key, previous);
}
return previous;
}
}
@Override
public Collection<String> keys() {//获取key的set集合
synchronized (this) {
return new HashSet<String>(map.keySet());
}
}
@Override//清空map
public void clear() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* Returns the size {@code Bitmap} in bytes.
* <p/>
* An entry's size must not change while it is in the cache.
* 计算size方法
*/
private int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
public synchronized final String toString() {
return String.format("LruCache[maxSize=%d]", maxSize);
}
}
设计还是比较清晰简单的,主要是通过LinkedHashMap
来进行存储。
如果内存缓存中获取不到bitmap,那么将根据uri从本地或网络进行加载,所有的信息都封装在LoadAndDisplayImageTask
类中,该类实现了Runnable
接口,因此整个过程是在run
方法进行的:
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();//判断当前任务是否正常,view是否被回收,任务是否正常
//再次从内存中获取缓存的bitmap,个人理解是当请求任务很多时,很可能之前的线程,已经将图片缓存了,所以再次获取
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);//显示图片
}
这里看下bmp = configuration.memoryCache.get(memoryCacheKey);
,如果if (bmp == null || bmp.isRecycled())
那么就会走到bmp = tryLoadBitmap();
,我们看看这个方法:
/**
* 获取图片
* @return
* @throws TaskCancelledException
*/
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {//根据uri从本地缓存获取图片的缓存文件
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
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
//判断是否bitmap是否存在,如果不存在,那么将从网络进行加载
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;//做网络标记
String imageUriForDecoding = uri;
//如果设置了本地缓存开启,tryCacheImageOnDisk()方法会去网络获取图片,并缓存到本地
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();//检测任务是否正常
//如果设置了本地缓存开启,那么bitmap会从本地加载,如果没有则从网络加载
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);//如果加载失败,设置失败监听
}
}
} catch (IllegalStateException e) {
fireFailEvent(FailType.NETWORK_DENIED, null);//如果加载失败,设置失败监听
} catch (TaskCancelledException e) {
throw e;
} catch (IOException e) {
L.e(e);
fireFailEvent(FailType.IO_ERROR, e);
} catch (OutOfMemoryError e) {
L.e(e);
fireFailEvent(FailType.OUT_OF_MEMORY, e);
} catch (Throwable e) {
L.e(e);
fireFailEvent(FailType.UNKNOWN, e);
}
return bitmap;
}
通过上面的方法分析我们可以大致知道如果开启了本地缓存,那么将会执行tryCacheImageOnDisk()
先下载图片再本地缓存,而如果没用开启缓存,那么会执行decodeImage(imageUriForDecoding)
去网络下载图片,这里我们分析下通过本地缓存和网络加载两种方式,主要了解下两者的实现区别,看看tryCacheImageOnDisk()
方法:
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
loaded = downloadImage();//下载图片,并进行本地缓存
if (loaded) {//判断有没有下载成功
int width = configuration.maxImageWidthForDiskCache;
int height = configuration.maxImageHeightForDiskCache;
if (width > 0 || height > 0) {
L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
// 如果设置了本地图片缓存的最大宽高,默认为0,重新设置图片大小并再次本地缓存
resizeAndSaveImage(width, height);
}
}
} catch (IOException e) {
L.e(e);
loaded = false;
}
return loaded;
}
看看downloadImage()
方法:
private boolean downloadImage() throws IOException {
//根据uri来获取流
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
if (is == null) {
L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
return false;
} else {
try {//进行本地缓存
return configuration.diskCache.save(uri, is, this);
} finally {
IoUtils.closeSilently(is);
}
}
}
getStream()
方法默认由BaseImageDownloader
对象实现:
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://content provider
return getStreamFromContent(imageUri, extra);
case ASSETS://assets 目录
return getStreamFromAssets(imageUri, extra);
case DRAWABLE://图片
return getStreamFromDrawable(imageUri, extra);
case UNKNOWN:
default:
return getStreamFromOtherSource(imageUri, extra);
}
}
根据uri的前缀来选择不同的加载方式,这里我们看看从getStreamFromNetwork(imageUri, extra)`方法:
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra);//创建一个HttpURLConnection对象
int redirectCount = 0;//重定向次数
//最大5次重定向
while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
conn = createConnection(conn.getHeaderField("Location"), extra);
redirectCount++;
}
InputStream imageStream;
try {//获取图片流
imageStream = conn.getInputStream();
} catch (IOException e) {
// Read all data to allow reuse connection (http://bit.ly/1ad35PY)
IoUtils.readAndCloseStream(conn.getErrorStream());
throw e;
}
if (!shouldBeProcessed(conn)) {//如果状态码不是200,那么关闭流
IoUtils.closeSilently(imageStream);
throw new IOException("Image request failed with response code " + conn.getResponseCode());
}
//重新封装流
return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}
至此,就从网络获取到图片流了,然后转换为bitmap,然后进行本地缓存,如果设置了diskCacheSize
或者diskCacheFileCount
,只要满足任意一个条件,那么就会用LruDiskCache
实现,否者用UnlimitedDiskCache
实现,先看看UnlimitedDiskCache
吧,其实该类UnlimitedDiskCache
,就是BaseDiskCache
,这个对本地缓存没有大小限制,所以看看BaseDiskCache
的实现:
//根据图片uri,来保存uri
@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
File imageFile = getFile(imageUri);//获取缓存文件
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);//临时文件
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
boolean savedSuccessfully = false;
try {//将bitmap保存到临时文件
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
//如果保存成功,但重命名没有成功则保存失败
if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
savedSuccessfully = false;
}
//如果没有保存成功,则删除临时文件
if (!savedSuccessfully) {
tmpFile.delete();
}
}
bitmap.recycle();
return savedSuccessfully;
}
/** Returns file object (not null) for incoming image URI. File object can reference to non-existing file. */
//根据uri获取缓存图片文件
protected File getFile(String imageUri) {
String fileName = fileNameGenerator.generate(imageUri);//生成文件名
File dir = cacheDir;//缓存目录
if (!cacheDir.exists() && !cacheDir.mkdirs()) {//如果缓存目录不存在,那么就用备用缓存目录
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
return new File(dir, fileName);
}
主要就是这两个方法了,还是比较简单。
至于LruDiskCache
类的源码,内部其实是由JakeWharton的DiskLruCache
实现的,所以单独另开一篇分析此类,本文分析到此为止。
网友评论