UniversalImageLoader
[TOC]
本章主要介绍UniversalImageLoader的主要实现原理,和使用方法
在介绍之前,理解《Android开发艺术探索》的第十二章Bitmap的加载和Cache是十分重要的,因为该章节介绍了ImageLoader的最核心的工作原理,这样能够为更好地理解UIL打下坚实的基础。因此建议先去阅读理解该章的内容。
由该章的内容中可以知道,ImageLoader最重要的几点工作如下
- 图片的同步加载
- 图片的异步加载
- 图片压缩
- 内存缓存
- 磁盘缓存
- 网络拉取
因此我们从这几点出发,结合UniversalImageLoader关键部分的源码来具体分析UIL的实现原理以及如果使用。
1.UIL中主要工作类
从上面ImageLoader的主要工作中可以得知大概需要以下几个类:
- ImageLoader:外界使用的主要工作类
- 线程池:用于图片的网络加载
- Handler:用于图片的异步传送加载
- ImageView:用于加载图片的控件
- ImageResizer:用于图片的压缩
- LruCache:用于图片的内存缓存
- DiskLruCache:用于图片的磁盘缓存
对应以上的工作,UIL中重要的几个类有:
- ImageLoader:主要工作类
- ImageLoaderConfiguration:配置类,用于配置大部分工作的类成员,包括线程池成员,内存缓存成员,磁盘缓存成员,图片下载器,图片解码器和显示图片参数等等。该类的作用就是参与ImageLoader的初始化并配置好正常工作的参数,因此该类很重要
- ImageLoaderEngine:用于线程池的管理,是一个管理类。该类与ImageLoaderConfiguration类的区别是,Configuration只是用于配置线程池,而真正使用线程池工作的是ImageLoaderEngine类
- ImageLoadingInfo:用于配置显示图片的信息类,包括图片的url、图片的加载大小、显示图片的控件和用于配置显示图片的配置类DisplayImageOptions等等
- DisplayImageOptions:显示图片参数的配置类,其中包含了Handler用于异步加载,BitmapDisplayer用于最终显示图片。该类和ImageLoadingInfo的关系是,DisplayImageOptions包含于ImageLoadingInfo之中,毕竟DisplayImageOptions也是跟图片的显示参数有关系的
- LoadAndDisplayImageTask:封装了获取图片并显示的任务,用于被线程池执行
- ImageDownloader:用于下载图片,即获取图片的输入流
- ImageDecoder:用于图片解码压缩,可以理解成将输入流转换成Bitmap的过程,因此一般ImageDecoder和ImageDownloader是一起工作的,一个负责获取图片的输入流,一个负责将输入流转换成Bitmap图片对象,其中就包含了按需求压缩图片的功能
- MemoryCache:内存缓存类
- DiskCache:磁盘缓存类
UIL中的主要工作大体上是由前5个类完成的,其实也很好理解,ImageLoader是向外提供的主要接口,ImageLoaderConfiguration为ImageLoader正常工作而提供配置参数,ImageLoaderEngine是在ImageLoader中的主要工作成员,ImageLoadingInfo是为了配置最后显示图片的参数比如如何显示、显示在哪里之类的,毕竟ImageLoader顾名思义是跟图片的显示密不可分的,因此没有DisplayImageOptions怎么让图片得到正确的显示。
2.UIL的工作流程
在深入源码分析UIL之前,我们先来了解UIL的工作流程,这样带着大局观去看程序就不会陷入了源码中不能自拔,导致只见树木不见森林的情况。因为了解了整体的工作流程之后,在看源码的时候就能迅速的找到重点流程而忽略细节的处理,有助于能够迅速地大体将框架学习一遍。因此在学习任何源码之前可以的话,最好先去搜索相关的工作原理,在掌握了一定原理之后再去看源码则会达到事半功倍的效果。
UIL的工作流程其实跟《Android开发艺术探索》中ImageLoader的工作流程大致一样,就是利用3级缓存策略
内存缓存->磁盘缓存->网络拉取
接下来如果一开始就从头到尾分析源码的工作流程可能会导致思路不清晰,因为其中用到了许多类,如果在分析工作流程的过程中再去分析一个个类的作用会将思路中断,不利于对整个框架的学习,因此下面会先对一个个在工作流程中会被用到的一些比较关键的类进行分析,最后再通过源码将整个过程过一遍。
3.ImageLoaderConfiguration
ImageLoaderConfiguration作为一个配置类,对于ImageLoader正常工作来说必不可少,因为它主要是为了配置好使ImageLoader正常工作的各种类。
ImageLoaderConfiguration采用的是建造者Builder模式来创建对象,虽然它拥有很多类成员,但是有一些是在Configuration对象被创建的时候初始化好了的。
从下面的代码可以看出,在UIL内部,最重要的无外乎线程池Executor、内存缓存MemoryCache、磁盘缓存DiskCache、图片下载器ImageDownloader和图片显示配置参数DisplayImageOptions这几个类等等。
其中一些默认的参数是,线程池的大小默认是3,任务队列采用的是FIFO,有默认的图片下载器BaseImageDownLoader,图片解码器BaseImageDecoder和默认显示图片配置DisplayImageOptions
// 下面这些Configuration类中所有的可选方法,可以看出有些属性是默认的
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions
.diskCacheExtraOptions(480, 800, null)
.taskExecutor(...)
.taskExecutorForCachedImages(...)
.threadPoolSize(3) // default
.threadPriority(Thread.NORM_PRIORITY - 2) // default
.tasksProcessingOrder(QueueProcessingType.FIFO) // default
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(2 * 1024 * 1024))
.memoryCacheSize(2 * 1024 * 1024)
.memoryCacheSizePercentage(13) // default
.diskCache(new UnlimitedDiskCache(cacheDir)) // default
.diskCacheSize(50 * 1024 * 1024)
.diskCacheFileCount(100)
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default
.imageDownloader(new BaseImageDownloader(context)) // default
.imageDecoder(new BaseImageDecoder()) // default
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
.writeDebugLogs()
.build();
我们从源码中看默认项是怎么实现的,在源码中我们可以看到,在build()的时候会判断一些属性是否为null,如果为null则调用对应的创建方法构造一个默认对象。可以看出默认被构造的有taskExecutor、diskCache、memoryCache、downloader、decoder和defaultDisplayImageOptions等等,这些都是ImageLoader加载显示一个图片所需要的类。因此实际上不要手动配置Configuration,只需要一个build的configuration也能使ImageLoader正常工作。
/** Builds configured {@link ImageLoaderConfiguration} object */
public ImageLoaderConfiguration build() {
initEmptyFieldsWithDefaultValues();
return new ImageLoaderConfiguration(this);
}
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();
}
}
另:BitmapDisplayer 默认初始化在DisplayImageOptions中
Handler 默认初始化在ImageLoader的displayImage中,然后传送到LoadAndDisplayImageTask当中
DisplayImageOptions 默认初始化在ImageLoader的loadImage中
4.ImageLoaderEngine
线程池的管理类,因为主要的任务都是通过线程池来完成的,因此可以认为ImageLoader里面干活的就是ImageLoaderEngine类里面的线程池。
下面主要介绍ImageLoaderEngine的构造方法和两个主要工作方法
/**
* 构造方法,可以看到利用的是Configuration类当中的参数来配置本身的线程池成员
*/
ImageLoaderEngine(ImageLoaderConfiguration configuration) {
this.configuration = configuration;
taskExecutor = configuration.taskExecutor;
taskExecutorForCachedImages = configuration.taskExecutorForCachedImages;
taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
}
/** 执行任务的方法,可以看到内部是利用线程池成员来执行的*/
void submit(final LoadAndDisplayImageTask task) {
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
taskExecutorForCachedImages.execute(task);
} else {
taskExecutor.execute(task);
}
}
});
}
/** Submits task to execution pool */
void submit(ProcessAndDisplayImageTask task) {
initExecutorsIfNeed();
taskExecutorForCachedImages.execute(task);
}
5.LoadAndDisplayImageTask & DisplayBitmapTask
UIL工作中最主要的任务类,其中LoadAndDisplayImageTask包含了DisplayBitmapTask,从名称上就能看出关系,LoadAndDisplayImageTask是下载和显示图片,而DisplayBitmapTask只是显示显示图片。
其实这个任务类也是UIL的核心工作类,有必要深刻地理解其工作流程。
下面是LoadAndDisplayImageTask的run方法,这里跟之前学过ImageLoader的同步加载图片流程差不多,这里主要分为几步:
- 加上同步锁:loadFromUriLock.lock();
- 从内存缓存中获取图片:bmp = configuration.memoryCache.get(memoryCacheKey);
- 从磁盘缓存、网络中获取图片:bmp = tryLoadBitmap();
- 利用DisplayBitmapTask异步显示图片:runTask(displayBitmapTask, syncLoading, handler, engine);注意该方法中有利用到Handler用于异步加载图片,而该Handler是在ImageLoader的displayImage中被默认初始化的
final class LoadAndDisplayImageTask
{
...
@Override
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();
//从内存缓存中获取
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 displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
/**
* 该方法的作用是先从磁盘缓存里获取数据
* 如果没有则调用tryCacheImageOnDisk()从网络获取图片并将其存到磁盘中
* 然后再从磁盘中获取图片
* 因此整个流程是:
* 磁盘缓存获取->网络获取并存到磁盘缓存并从磁盘缓存获取
*/
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
//先从磁盘中获取图片
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 = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;
String imageUriForDecoding = uri;
//从网络上获取图片存到磁盘中
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
//从磁盘中获取
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();
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;
}
/**
* 从网络中获取图片存储到磁盘缓存中,通过downloadImage()实现的
* 而后的resizeAndSaveImage(width, height)是为了根据实际需求将图片压缩并重新存入磁盘缓存
*
* 下载成功则返回true
*/
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);
resizeAndSaveImage(width, height); // TODO : process boolean result
}
}
} catch (IOException e) {
L.e(e);
loaded = false;
}
return loaded;
}
/**
* 该方法主要分为两步:
* 1.从网络获取图片输入流:InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
* 其中Downloader在Configuration被创建的时候被默认初始化成BaseImageDownloader
* 2.通过输入流将图片存入磁盘缓存:configuration.diskCache.save(uri, is, this);
*/
private boolean downloadImage() throws IOException {
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);
}
}
}
}
上面的任务是图片的同步加载过程,这是通过线程池执行的,最后还要将图片显示出来,此时就需要转到主线程设置图片,也就是通过Handler来操作,而显示图片的过程是在DisplayBitmapTask中实现的,下面来看一下DisplayBitmapTask的运行过程。
final class DisplayBitmapTask
{
/**
* 在该方法中,主要的作用是在加载图片的过程中触发各个回调函数
* 其中最主要的是通过displayer将图片加载显示到imageAware控件当中
*
* 注:该任务应该运行在主线程
*/
@Override
public void run() {
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
//将图片bitmap加载到imageAware中用于显示
displayer.display(bitmap, imageAware, loadedFrom);
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
}
上面DisplayBitmapTask只是定义好了如何加载图片到控件当中,真正被转移到主线程中执行还是在LoadAndDisplayImageTask中的runTask方法中,可以看到,runTask方法中是通过参数中的handler将DisplayBitmapTask执行到主线程中的。
final class LoadAndDisplayImageTask
{
...
@Override
public void run() {
...
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
//displayBitmapTask执行
runTask(displayBitmapTask, syncLoading, handler, engine);
}
/**
* 从该方法中可以发现,displayBitmapTask是通过handler的post达到运行在主线程中加载图片的效果
*/
static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
if (sync) {
r.run();
} else if (handler == null) {
engine.fireCallback(r);
} else {
handler.post(r);
}
}
}
6.ImageLoadingInfo
ImageLoadingInfo信息类的作用类似于ImageLoaderConfiguration配置类,只是担当了信息参数储存的角色,实际并不做任何工作。
比如ImageLoaderConfiguration配置类是为ImageLoader正常工作流程提供参数,比如线程池、缓存和下载器等等。
而ImageLoadingInfo的作用则是为图片的加载显示提供参数,比如图片的url、图片的显示控件、目标显示大小和显示配置类DisplayImageOptions等等,注意DisplayImageOptions的作用是提供在显示图片时所需的参数和一些回调接口,下面有详细说明。 这里只需要记住ImageLoadingInfo的作用就是跟图片加载显示有关。
final class ImageLoadingInfo {
final String uri;
final String memoryCacheKey;
final ImageAware imageAware;
final ImageSize targetSize;
final DisplayImageOptions options;
final ImageLoadingListener listener;
final ImageLoadingProgressListener progressListener;
final ReentrantLock loadFromUriLock;
public ImageLoadingInfo(String uri, ImageAware imageAware, ImageSize targetSize, String memoryCacheKey,
DisplayImageOptions options, ImageLoadingListener listener,
ImageLoadingProgressListener progressListener, ReentrantLock loadFromUriLock) {
this.uri = uri;
this.imageAware = imageAware;
this.targetSize = targetSize;
this.options = options;
this.listener = listener;
this.progressListener = progressListener;
this.loadFromUriLock = loadFromUriLock;
this.memoryCacheKey = memoryCacheKey;
}
}
7.DisplayImageOptions
DisplayImageOptions跟图片的最终显示密切相关,而上面ImageLoadingInfo只是提供了一些参数,但并不直接跟图片显示有关系,DisplayImageOptions与图片如何正确显示才是密切相关的,因此配置好DisplayImageOptions是ImageLoader正确工作的重要一环,跟ImageLoaderConfiguration一样对于ImageLoader来说是必不可少的。
可以说在ImageLoader中,ImageLoaderConfiguration决定了图片如何加载,DisplayImageOptions决定了图片如何正确显示,每张图片的加载都离不开DisplayImageOptions的配置参数。
以下是跟ImageLoaderConfiguration一样的所有可选的显示图片参数配置。其中较重要的参数有cacheInMemory,cacheOnDisk这两个参数默认是false,即不使用缓存,displayer决定图片显示的样式,这几个一般在创建Options对象时需要重新配置,尤其是缓存。
注:如果DisplayImageOptions没有手动地创建赋予ImageLoader,则在ImageLoaderConfiguration中会默认创建一个Options对象,下面程序中后面带有default的就是默认配置的参数。
// DON'T COPY THIS CODE TO YOUR PROJECT! This is just example of ALL options using.
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub) // resource or drawable
.showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
.showImageOnFail(R.drawable.ic_error) // resource or drawable
.resetViewBeforeLoading(false) // default
.delayBeforeLoading(1000)
.cacheInMemory(false) // default
.cacheOnDisk(false) // default
.preProcessor(...)
.postProcessor(...)
.extraForDownloader(...)
.considerExifParams(false) // default
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
.bitmapConfig(Bitmap.Config.ARGB_8888) // default
.decodingOptions(...)
.displayer(new SimpleBitmapDisplayer()) // default
.handler(new Handler()) // default
.build();
8.ImageDownloader
图片下载器,即通过图片的URI来获取到图片的InputStream。ImageDownloader是一个interface,主要方法就只有getStream(String imgUri, Object extra),即外界通过该方法便可通过imgUri获取到图片的InputStream。UIL中有实现了该接口的BaseImageDownloader,我们来看看它是怎么实现的。
关注重点:
- ImageDownloader的作用主要是向外提供一个下载的接口方法getStream(String imgUri, Object extra)
- 默认连接超时时间为5s,读取超时时间为20s
- 可以从多个地方比如网络、文件、Content、Assets等地方获取图片输入流
- getStreamFromNetwork从网络获取图片输入流:
- 采用HttpURLConnection作为连接类
- 如果服务器返回3xx则进行重定向
- 将InputStream封装成ContentLengthInputStream返回
- getStreamFromFile从文件获取图片输入流
- ...
总的来说,ImageDownLoader的作用就是从图片的uri获取到相应的InputStream
public class BaseImageDownloader implements ImageDownloader {
//连接超时时间
public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 5 * 1000; // milliseconds
//读取超时时间
public static final int DEFAULT_HTTP_READ_TIMEOUT = 20 * 1000; // milliseconds
//缓存大小
protected static final int BUFFER_SIZE = 32 * 1024; // 32 Kb
//允许使用的用于URI中的字符
protected static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
//最大重定向的次数,用于在服务器返回3xx时进行重定向
protected static final int MAX_REDIRECT_COUNT = 5;
...
/**
* 构造方法,可以自定义连接超时和读取超时时间
* 默认分别是5s和20s
*/
public BaseImageDownloader(Context context) {
this(context, DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT);
}
public BaseImageDownloader(Context context, int connectTimeout, int readTimeout) {
this.context = context.getApplicationContext();
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
}
/**
* 实现ImageLoader方法,也是最主要的方法
* 可以看出可以分别从网络、文件、Content、Assets等等地方获取图片输入流
* 其中extra是传到DisplayImageOptions.Builder中的extraForDownloader,该参数可以为null
*/
@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);
}
}
/**
* 从网络的uri获取到InputStream
*
*/
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++;
}
//通过HttpURLConnection获取InputStream
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());
}
//创建HttpURLConnection连接
protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
return conn;
}
/**
* 从文件获取图片输入流
*/
protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException {
String filePath = Scheme.FILE.crop(imageUri);
if (isVideoFileUri(imageUri)) {
return getVideoThumbnailStream(filePath);
} else {
BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE);
return new ContentLengthInputStream(imageStream, (int) new File(filePath).length());
}
}
...
}
9.ImageDecoder
ImageDecoder是用于图片的解码,说是解码这么高大上,其实就是将ImageLoader获取到的图片InputStream转换成Bitmap而已,概括来说,就是直接利用BitmapFactory.decodeStream()就能完成该功能。而ImageDecoder则可以理解成将图片按照需求解码,主要是按照需求计算采样率、缩放和旋转图片。
主要关注重点:
- ImageDecoder只向外提供了一个解码图片的接口方法decode(ImageDecodingInfo decodingInfo)
- ImageDecodingInfo是一个存储相关的解码参数的信息类,跟ImageLoadingInfo一样只是用于存储配置参数,并不工作,只是在图片解码的过程中提供相应的参数,这些参数对解码来说都是必不可少的
- 根据ImageDecodingInfo对象获取到设置图片的采样率、缩放、旋转等等属性参数
- 通过decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);获取到符合采样率条件的Bitmap
- 通过decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);获取到最终符合缩放和旋转条件的Bitmap
总结一下ImageDecoder的作用就是将ImageLoader获取到的图片的输入流InputStream转换成满足需求的Bitmap对象
ImageLoader+ImageDecoder便可以通过图片的Url获取到满足需求的Bitmap对象
public class BaseImageDecoder implements ImageDecoder {
...
/**
* 将图片从URI中解码出来,其中图片是符合需要的采样率、缩放、旋转等需求的
*/
@Override
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
//获取图片输入流
InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
return null;
}
try {
//从ImageDecodingInfo对象当中提取解码参数将其设置到Options当中
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
//通过将Options获取到符合条件的Bitmap
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
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信息类
可以看出其中跟图片相关的属性有ImagegeSize,ImageScaleType,Options等等,这些属性决定了图片被解码出来的格式,比如大小,规模,采样率等等。
public class ImageDecodingInfo {
private final String imageKey;
private final String imageUri;
private final String originalImageUri;
private final ImageSize targetSize;
private final ImageScaleType imageScaleType;
private final ViewScaleType viewScaleType;
private final ImageDownloader downloader;
private final Object extraForDownloader;
private final boolean considerExifParams;
private final Options decodingOptions;
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);
}
}