这就是一个非常好用的图片加载框架,目前我们项目中用的就是它。
源码地址:https://github.com/nostra13/Android-Universal-Image-Loader
一.整体架构:
先说怎么看整个源码:我是根据下面这种图开始看的,另外,我觉得作者源码包的分类也非常明确清晰,这个都可以帮助我们一步一步来看整个框架是怎么构建而成的。
二.分步解析
1. Download Image:
这个包中,主要是将不同图片来源(http/https, file,contentprovider,asserts,drawable目录),都解析成InputStream 。针对多媒体也有获取到帧的方法。作者建议是对于drawable目录的图片,调用android原生方法就好,因为这个框架针对这种资源也是在调用android原生方法。
详见如下代码:
@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);
}
}
我们日常中的图片都是从网络中获取的,那么针对http/https图片的获取,框架底层是通过HttpURLConnection 进行的。
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra);
int redirectCount = 0;
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)) {
IoUtils.closeSilently(imageStream);
throw new IOException("Image request failed with response code " + conn.getResponseCode());
}
return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}
不同资源途径下的图片如何解析成InputStream就差不多分析完毕,也就是图片下载这个工作差不多告一段落,现在看图片加载完毕之后,怎么保存的问题。
2.图片的缓存:
看包的目录,可知作者做了两种类型的缓存,一种是磁盘缓存,一种是内存缓存。并且针对这些缓存,提供了不同算法实现方式。
2.1 磁盘缓存:
以LimitedAgeDiskCache 分析为例:
这个机制是缓存大小不受限制,但是会有缓存时限。
@Override
public File get(String imageUri) {
File file = super.get(imageUri);
if (file != null && file.exists()) {
boolean cached;
Long loadingDate = loadingDates.get(file);//获取缓存的文件的时间
if (loadingDate == null) {//没有缓存的情况
cached = false;
loadingDate = file.lastModified();
} else {//有缓存的情况
cached = true;
}
if (System.currentTimeMillis() - loadingDate > maxFileAge) {//如果文件已经超过了缓存的期限,删掉该文件
file.delete();
loadingDates.remove(file);
} else if (!cached) {//没有缓存的情况下,并且还在期限内:将该文件缓存下来
loadingDates.put(file, loadingDate);
}
}
return file;
}
//文件存储:
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
boolean saved = super.save(imageUri, imageStream, listener);
rememberUsage(imageUri);
return saved;
}
private void rememberUsage(String imageUri) {
File file = getFile(imageUri);
long currentTime = System.currentTimeMillis();
file.setLastModified(currentTime);//更新文件的最新时间
loadingDates.put(file, currentTime);
}
另外还有一种:LruDiskCache ,android原生中有LruCache,里面封装了一个LinkedHashMap,这种缓存策略是有大小限制的,再次用过的元素放在队尾,当大小超过maxsize的时候,将队头元素删除,这样既不会超过尺寸,也保证删除的是最近最少使用的,原理大致如此。
2.2 内存缓存:
内存缓存:其中的LRULimitedMemoryCache,里面封装了一个LinkedHashMap,按照访问顺序排列,既在get方法 的时候,重新将该元素放在队尾:
private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));
@Override public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
3.Image Decoder
这个业务是将图像解码为Bitmap,并将其缩放到所需大小。
@Override
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
InputStream imageStream = getImageStream(decodingInfo);//获取图片流,这里就回到了第一次的分析:采用imageloader下载图片流
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;
}
4.Bitmap Processor:
在这个步骤,就是关于图片如何处理的问题了。
不过这个是一个接口,由调用者自己去实现。
5. Bitmap Display:
图片展示过程有两种方法:
第一种是针对图片的展示displayImage ,调用如下的方法:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();//检查ImageLoaderConfiguration 配置,如果用户不想自己去配置的话,作者提供了一个DefaultConfigurationFactory ,里面有默认做线程,缓存dowmloadimage,display等相关配置。
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {//图片的展示配置,比如图片uri异常问题,图片处理等等
options = configuration.defaultDisplayImageOptions;
}
......
......
//获取图片缓存相关代码
......
......
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));//底层调用LoadAndDisplayImageTask
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);
}
}
}
第二种是loadImage,往线程池中 增加了图片下载的任务,最后可以看到会调用上面的displayImage方法。
public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (targetImageSize == null) {
targetImageSize = configuration.getMaxImageSize();
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
displayImage(uri, imageAware, options, listener, progressListener);
}
图片加载一般是耗时的,所以也是异步的,不过该框架也提供了一个同步的加载方法:
public Bitmap loadImageSync(String uri, ImageSize targetImageSize, DisplayImageOptions options) {
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
options = new DisplayImageOptions.Builder().cloneFrom(options).syncLoading(true).build();
SyncImageLoadingListener listener = new SyncImageLoadingListener();
loadImage(uri, targetImageSize, options, listener);
return listener.getLoadedBitmap();
}
//从loadImage中可以追踪到displayimage 中一个方法:
if (options.isSyncLoading()) {//如果是同步,交给这个线程去执行,执行完毕再执行下一个任务
displayTask.run();
} else {//如果是异步,交给线程池去执行,多个任务可以同时执行
engine.submit(displayTask);
}
还有最后一个补充点就是关于bitmap display:
框架中提供了如下几种方法:默认是用SimpleBitmapDisplayer ,不做任何处理,但是如果需要对图片四个角做圆角处理的话,可以选择用RoundedBitmapDisplayer
总结:
从整体来看,该框架中最为重要的是图片加载和图片缓存,特别是图片缓存这块。不过为了满足开发需求,作者也已经提供了多种算法的缓存策略以满足实际开发需要。我对这个的源码分析可能有些不准确和完整,但是却是自己第一次尝试去做这件事,看的过程中对作者框架的构思也有了一定的了解。