Android-Universal-Image-Loader阅读笔记(一)
上一篇主要介绍了一些参数和基础的用法
简述
上一篇有提到在displayImage的时候,首先会在调用线程中检查是否击中内存缓存,如果击中内存缓存,在没有指定postProcessor的情况下就直接在当前线程回调结果。这个过程如果是在主线程中进行,则整个任务都会完结在主线程中。
如果没有击中内存缓存,接着任务就到了engine.submit(displayTask)中,接下来看一下ImageLoaderEngine
ImageLoaderEngine
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();//如果ImageLoader之前进行了stop,那么这里要尝试使用可用的线程池
if (isImageCachedOnDisk) {//当前命中硬盘缓存,通过专门处理缓存的线程池执行任务
taskExecutorForCachedImages.execute(task);
} else {//当前没有命中硬盘缓存,通过专门处理从流(网络等来源)中获取图片的线程池执行任务
taskExecutor.execute(task);
}
}
});
}
/**
* 暂停后续的图片加载任务,当前已经进行中的任务无法暂停
* 只能通过resume恢复暂停后添加的新任务(重复的任务会不断的覆盖)
*/
void pause() {
paused.set(true);//实际上就是标记一下当前ImageLoaderEngine暂停
}
/**
* 继续图片的加载任务,并且会尝试唤醒之前暂停的任务
*/
void resume() {
paused.set(false);//标记当前ImageLoaderEngine可以运行
synchronized (pauseLock) {
pauseLock.notifyAll();//尝试唤醒之前因为pause而处于wait状态的加载线程,从而开始之前的任务
}
}
简要说就是当加载一个图片的时候,首先在UI线程中尝试击中内存缓存,如果没有击中,会立刻在子线程中尝试击中硬盘缓存,如果击中,通过缓存专用的线程池调度到另一个线程中执行task,否则通过流专用的线程池调度到另一个线程中执行task,总之就是到不同线程中执行同一个任务。
接着看任务LoadAndDisplayImageTask本身
LoadAndDisplayImageTask
实际上就是一个Runnable,用于在线程池中执行
@Override
public void run() {
//ImageLoaderEngine是否被暂停,如果暂停,则当前任务直接结束
//不过还是可以从内存缓存中获取图片,这个在列表页滑动加载还是很有意义的
//因为线程池默认是fixed类型的线程池,那么pause的时候最多会导致核心线程数的等待
//后续任务进入都会进行线程池的队列中等待执行
if (waitIfPaused()) return;
//如果配置了当前任务需要延时读取,则当前线程会沉睡指定毫秒后唤醒,如果任务有效则继续执行
if (delayIfNeed()) return;
//这个是当前链接对应的线程锁
//一个连接对应有一个ReentrantLock
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 {
//对于同一个链接的任务,有的任务在等待后重新执行,此时可能过了一段时间
//需要检查任务的有效性,如果无效直接进入catch
checkTaskNotActual();
//再次尝试从内存缓存中获取
//主要场景就是当多个相同链接的请求发生的时候,只有一个任务正常执行,其余任务阻塞
//那么当第一个任务完成之后,下一个任务唤醒之后,此时可能因为第一个任务的成功而导致内存缓存中有值
//此时从内存缓存中获取即可,不必要再次进行多余操作
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {//内存缓存中还是没有数据
//从硬盘或者网络上尝试获取Bitmap
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
//bitmap在缓存到内存缓存中之前可能要进行preProcessor操作
//在这之前先检查任务的有效性
checkTaskNotActual();
checkTaskInterrupted();
//后续操作和硬盘缓存已经没有关系
//在进行内存缓存前可以对Bitmap进行操作
//这个对应于从内存缓存中获取bitmap之后的postProcessor
if (options.shouldPreProcess()) {
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
}
//当前允许将bitmap存入内存缓存中
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);
}
//这里的bitmap可以看做从内存缓存中获取的,
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();//这个异常仅对应与任务取消异常,会回调onLoadingCancelled
return;
} finally {//注意ReentrantLock的基础,必须在try/catch/finally模块中,否则可能出现锁无法释放的情况
loadFromUriLock.unlock();
}
//进行展示任务
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
/**
* 如果有必要,wait当前线程
* */
private boolean waitIfPaused() {
AtomicBoolean pause = engine.getPause();
if (pause.get()) {//获取当前线程池是否暂停的标志
//如果当前线程池被暂停
synchronized (engine.getPauseLock()) {//暂停锁
if (pause.get()) {//因为锁的原因,可能会导致阻塞了一些时间,这里需要进行二次检查
L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
try {
//如果当前线程池需要暂停,释放engine.getPauseLock()的锁,并且当前线程等待执行
//ImageLoaderEngine在resume中通过engine.getPauseLock().notifyAll即可唤醒当前所有在等待中的线程
engine.getPauseLock().wait();
} catch (InterruptedException e) {
L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
return true;
}
L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
}
}
}
//当任务被唤醒时,因为可能隔了一段时间,重新检查一下当前任务的有效性
return isTaskNotActual();
}
/**
* 如果需要,尝试延时当前线程的执行
* */
private boolean delayIfNeed() {
//如果在DisplayOption中配置了延时读取的毫秒
if (options.shouldDelayBeforeLoading()) {
L.d(LOG_DELAY_BEFORE_LOADING, options.getDelayBeforeLoading(), memoryCacheKey);
try {//线程沉睡指定毫秒
Thread.sleep(options.getDelayBeforeLoading());
} catch (InterruptedException e) {
L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
return true;
}
//唤醒后,因为过了一段时间,需要重新检查当前任务的有效性
return isTaskNotActual();
}
return false;
}
/**
* 尝试从硬盘和网络上获取Bitmap
* @return 获取的bitmap
* @throws TaskCancelledException 当前任务已经无效异常
*/
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
//从硬盘缓存中根据链接获取指定文件
File imageFile = configuration.diskCache.get(uri);
//该文件是可读性质的,同时要求长度>0
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {//击中硬盘缓存
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;
//即将进行图片的压缩等处理,先检查任务的有效性
checkTaskNotActual();
//根据uri解析bitmap,这个Scheme中定义了ImageLoader可以识别的前缀,具体看Scheme类
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;
//DisplayOptions中设置允许缓存在硬盘中的话,尝试从网络上加载图片并且缓存在硬盘中
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
//尝试从硬盘中获取刚刚通过网络等方式获取的图片
//这里除非在ImageLoaderConfiguration中指定硬盘缓存中的最大宽高
//否则一般来说就是原图
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
//获取成功后需要添加file的Scheme
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
//准备进行拉伸压缩等操作,先检查任务的有效性
checkTaskNotActual();
//如果允许硬盘缓存的话,再次解析文件,压缩等操作(之前进行过压缩,所以这里基本上就是过一遍判断)
//否则就是从网络上获取流,然后压缩等操作,不会进行硬盘缓存
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
//普通的异常意味着获取图片失败,会设置fail时候设置的图片,并且回调onLoadingFailed
//有一个异常比较特殊,任务取消异常,这个会在上一级catch中捕捉
} 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;
}
/**
* 根据uri解析图片(uri可能是http、drawable、content等,具体看Scheme类)
* @param imageUri 当前要处理的图片URI
* @return 压缩拉伸等处理后的Bitmap
*/
private Bitmap decodeImage(String imageUri) throws IOException {
//获取压缩类型CROP或FIT_INSIDE
ViewScaleType viewScaleType = imageAware.getScaleType();
//创建解析Bitmap所需要的参数
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
//默认decoder在ImageLoaderConfiguration中创建BaseImageDecoder
return decoder.decode(decodingInfo);
}
/**
* 尝试将图片缓存到硬盘中
* */
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
//从网络上加载图片,并且缓存进硬盘当中
loaded = downloadImage();
if (loaded) {//缓存成功
//如果配置当中设置了硬盘缓存的最大宽高,则需要改变bitmap的宽高并且覆盖缓存
int width = configuration.maxImageWidthForDiskCache;
int height = configuration.maxImageHeightForDiskCache;
if (width > 0 || height > 0) {//注意这里的resize会影响到内存缓存的结果,因为内存缓存是在最后面操作
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;
}
/**
* 从网络(或者文件之类)上获取图片的输入流,并且保存进硬盘当中
* @return true表示硬盘缓存成功
*/
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);
}
}
}
/**
* 将硬盘缓存中的数据重新设置宽高,然后覆盖
* 用于设置硬盘缓存中的最大宽高
* */
private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException {
boolean saved = false;
File targetFile = configuration.diskCache.get(uri);
//从网络或者其它来源加载图片输入流成功之后,会先将流存入硬盘缓存中
//这里会尝试再次取出
if (targetFile != null && targetFile.exists()) {
ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
//指定IN_SAMPLE_INT的时候,只会进行压缩处理,不会拉伸
DisplayImageOptions specialOptions = new DisplayImageOptions.Builder().cloneFrom(options)
.imageScaleType(ImageScaleType.IN_SAMPLE_INT).build();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey,
Scheme.FILE.wrap(targetFile.getAbsolutePath()), uri, targetImageSize, ViewScaleType.FIT_INSIDE,
getDownloader(), specialOptions);
//根据给定的新的宽高重新拉伸压缩等操作
Bitmap bmp = decoder.decode(decodingInfo);
if (bmp != null && configuration.processorForDiskCache != null) {
L.d(LOG_PROCESS_IMAGE_BEFORE_CACHE_ON_DISK, memoryCacheKey);
bmp = configuration.processorForDiskCache.process(bmp);
if (bmp == null) {
L.e(ERROR_PROCESSOR_FOR_DISK_CACHE_NULL, memoryCacheKey);
}
}
if (bmp != null) {//将处理后的bitmap重新写入硬盘缓存中进行覆盖
saved = configuration.diskCache.save(uri, bmp);
bmp.recycle();
}
}
return saved;
}
看到这里,ImageLoader的整个加载流程就已经清楚了。
1.先判断当前uri是否为空,如果为空,取消同一个ImageAware的请求,并且设置链接为空的时候的默认图。否则继续步骤2
2.确定想要的图片宽高,标记当前ImageAware的请求(可以使得旧的请求失效),从内存缓存中获取bitmap,获取成功,执行展示任务展示图片即可。否则继续步骤3
3.首先展示设置的加载中默认图或者重置当前载体中的图片,然后进入子线程中尝试从硬盘缓存中获取缓存,然后进入另一个子线程中处理。
4.判断当前ImageLoader是否暂停,如果暂停,wait当前线程。否则继续步骤5
5.判断当前加载任务是否需要延时,如果需要,让当前线程sleep指定时间,然后继续步骤6
6.获取当前链接对应的锁,否则等待。
7.从内存缓存中获取bitmap,命中缓存则开始展示任务,否则继续步骤8
8.从硬盘缓存中获取图片,如果没有命中缓存,从网络等来源获取图片的输入流,然后将原图先缓存到硬盘缓存中,如果设置了硬盘缓存的最大宽高,会重新读取一次硬盘缓存,然后压缩拉伸图片,最后再存入硬盘缓存中。
9.进行压缩拉伸等处理,然后如果允许缓存到内存缓存中,将当前压缩处理后的bitmap缓存到内存缓存中
10.最后执行展示图片任务即可
ImageDownloader
用于通过URI获取图片的输入流
/**
* 从指定的URI中获取图片的输入流
*
* @param imageUri 图片的URI
* @param extra 在DisplayImageOptions中通过extraForDownloader指定的参数
* @return 图片的输入流
*/
InputStream getStream(String imageUri, Object extra) throws IOException;
public enum Scheme {
HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("");
根据Scheme来看,ImageLoader支持从网络上面来的图片、硬盘上的图片、ContentProvider、assets目录下的图片和资源文件。
看一下ImageLoader默认的Downloader实现
BaseImageDownloader
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
//根据不同的Scheme获取对应的流,一个个看
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
//网络
return getStreamFromNetwork(imageUri, extra);
case FILE:
//文件,主要用于硬盘缓存,当然也可以自己写文件路径
return getStreamFromFile(imageUri, extra);
case CONTENT:
//内容提供者,当然就包括一些文件,里面主要是视频的缩略图、联系人的头像等,当然主要是照相和选择图片的uri
return getStreamFromContent(imageUri, extra);
case ASSETS:
//assets中的资源
return getStreamFromAssets(imageUri, extra);
case DRAWABLE:
//res中的图片资源
return getStreamFromDrawable(imageUri, extra);
case UNKNOWN:
default:
//异常处理,直接抛出异常
return getStreamFromOtherSource(imageUri, extra);
}
}
/**
* 从网络上获取对应输入流
*/
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra);
int redirectCount = 0;
//状态码如果是300-399的状态,认为当前链接存在重定向的行为,此时重定向之后会将新的地址放入报头的Location中
//之所以最多5次,是因为HttpURLConnection内部最多允许5次重定向http://blog.csdn.net/flowingflying/article/details/18731667
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)
//如果链接成功但是出现了异常,Connection仍然会返回一个错误流,此时为了继续使用Connection,最好将流全部读取并且关闭
IoUtils.readAndCloseStream(conn.getErrorStream());
throw e;
}
//判断当前返回标志是否为200
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());
}
/**
* HttpURLConnection返回200状态码才认为本次网络连接成功
*/
protected boolean shouldBeProcessed(HttpURLConnection conn) throws IOException {
return conn.getResponseCode() == 200;
}
/**
* 根据指定的http协议的url创建指定的HttpURLConnetion
*/
protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
//这里对链接进行了encode操作,当中指定了一些需要避免encode的字符
String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
//默认连接超时5s,读取图片输入流超时20s
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
return conn;
}
/**
* 返回来源于文件的图片的输入流
*/
protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException {
//获取文件的绝对路径(不包含Scheme)
String filePath = Scheme.FILE.crop(imageUri);
if (isVideoFileUri(imageUri)) {
//如果是video类型文件,尝试通过video文件创建一个缩略图
return getVideoThumbnailStream(filePath);
} else {//普通的文件
BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE);
return new ContentLengthInputStream(imageStream, (int) new File(filePath).length());
}
}
/**
* 根据video文件路径生成缩略图,并且获得缩略图的输入流
*/
@TargetApi(Build.VERSION_CODES.FROYO)
private InputStream getVideoThumbnailStream(String filePath) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
//创建缩略图
Bitmap bitmap = ThumbnailUtils
.createVideoThumbnail(filePath, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND);
if (bitmap != null) {
//将bitmap转为输入流返回
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.PNG, 0, bos);
return new ByteArrayInputStream(bos.toByteArray());
}
}
return null;
}
/**
* 通过内容提供者的uri形式获取图片的输入流
*/
protected InputStream getStreamFromContent(String imageUri, Object extra) throws FileNotFoundException {
ContentResolver res = context.getContentResolver();
Uri uri = Uri.parse(imageUri);
if (isVideoContentUri(uri)) {//当前uri为video类型
//video类型只获取对应缩略图
Long origId = Long.valueOf(uri.getLastPathSegment());
Bitmap bitmap = MediaStore.Video.Thumbnails
.getThumbnail(res, origId, MediaStore.Images.Thumbnails.MINI_KIND, null);
if (bitmap != null) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.PNG, 0, bos);
return new ByteArrayInputStream(bos.toByteArray());
}
} else if (imageUri.startsWith(CONTENT_CONTACTS_URI_PREFIX)) {
//如果uri是联系人类型,返回联系人的头像
return getContactPhotoStream(uri);
}
//对于一般的图片来说,直接通过ContentResolver和Uri打开输入流即可
//比方说系统的选择图片之类的
return res.openInputStream(uri);
}
/**
* 通过制定URI获取联系人头像的输入流
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected InputStream getContactPhotoStream(Uri uri) {
ContentResolver res = context.getContentResolver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
return ContactsContract.Contacts.openContactPhotoInputStream(res, uri, true);
} else {
return ContactsContract.Contacts.openContactPhotoInputStream(res, uri);
}
}
/**
* 从assets中读取文件的输入流
*/
protected InputStream getStreamFromAssets(String imageUri, Object extra) throws IOException {
String filePath = Scheme.ASSETS.crop(imageUri);
//通过这种形式可以直接是相对路径
return context.getAssets().open(filePath);
}
/**
* 通过drawable://123123这种类型的uri获取资源文件的输入流
*/
protected InputStream getStreamFromDrawable(String imageUri, Object extra) {
//这里要求的是drawable://123123,其中12313应该是资源id,通过R.drawable.icon获取
String drawableIdString = Scheme.DRAWABLE.crop(imageUri);
int drawableId = Integer.parseInt(drawableIdString);
return context.getResources().openRawResource(drawableId);
}
这个只是ImageLoader的默认实现,一般来说可以自己实现从网络上获取图片,结合自己项目中使用的网络库。
最后看一下图片的压缩拉伸处理
ImageDecoder
当从网络上获取图片之后,因为原图有的时候可能远远大于载体,处于对内存的考虑也是应该进行压缩等操作的,而ImageDecoder就是做这件事情的。
public interface ImageDecoder {
/**
* 根据指定的目标大小和参数处理图片
* @param imageDecodingInfo 处理的时候可能需要使用的参数
* @return 处理后的bitmap
*/
Bitmap decode(ImageDecodingInfo imageDecodingInfo) throws IOException;
}
可以看到,职责很单一,就是通过给定的参数对bitmap进行处理,接着看默认的实现BaseImageDecoder
@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 {
//根据Bitmap和图像的方向获得图片大小等信息
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
//BitmapFactory是不支持多次使用同一个流的,所以要么重置,要么重新来获取图片的输入流
//默认的网络使用的是BufferedInputStream,是可以重置流的,不需要重新发起网络请求
imageStream = resetStream(imageStream, decodingInfo);
//根据当前获得的bitmap的宽高和载体的宽高,计算并设置Options的压缩比例,主要是inSampleSize
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
//解析流获得bitmap
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;
}
protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
throws IOException {
Options options = new Options();
//当前不会将bitmap的数据加载入内存,只会获得相关参数,避免造成内存过大的占用
options.inJustDecodeBounds = true;
//解析当前流,并且将数据记录在Options中,主要是为了记录原图的宽高
BitmapFactory.decodeStream(imageStream, null, options);
ExifInfo exif;
String imageUri = decodingInfo.getImageUri();
//默认一般的图片方向都是不考虑的,但是比方说有的自定义使用照相API的时候拍的图片可能要考虑
if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
exif = defineExifOrientation(imageUri);
} else {
exif = new ExifInfo();
}
return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
}
/**
* 首先是本地的图片,而且jpeg格式的图片才需要考虑图片的方向问题
* 这个场景比方说手机照相
*/
private boolean canDefineExifParams(String imageUri, String mimeType) {
return "image/jpeg".equalsIgnoreCase(mimeType) && (Scheme.ofUri(imageUri) == Scheme.FILE);
}
/**
* 获取图片流,这种形式从文件或者网络获取都有可能
*/
protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
}
/**
* 判断当前图片的方向决定旋转的角度之类
*/
protected ExifInfo defineExifOrientation(String imageUri) {
int rotation = 0;
boolean flip = false;
try {
ExifInterface exif = new ExifInterface(Scheme.FILE.crop(imageUri));
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (exifOrientation) {
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
flip = true;
case ExifInterface.ORIENTATION_NORMAL:
rotation = 0;
break;
case ExifInterface.ORIENTATION_TRANSVERSE:
flip = true;
case ExifInterface.ORIENTATION_ROTATE_90:
rotation = 90;
break;
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
flip = true;
case ExifInterface.ORIENTATION_ROTATE_180:
rotation = 180;
break;
case ExifInterface.ORIENTATION_TRANSPOSE:
flip = true;
case ExifInterface.ORIENTATION_ROTATE_270:
rotation = 270;
break;
}
} catch (IOException e) {
L.w("Can't read EXIF tags from file [%s]", imageUri);
}
return new ExifInfo(rotation, flip);
}
/**
* 准备实际上用于解析Bitmap的参数
* @param imageSize 需要解析的Bitmap的原大小
* @param decodingInfo 解析参数
*/
protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
ImageScaleType scaleType = decodingInfo.getImageScaleType();
int scale;
//根据配置中的压缩模式来计算压缩比例
if (scaleType == ImageScaleType.NONE) {//不进行压缩
scale = 1;
} else if (scaleType == ImageScaleType.NONE_SAFE) {
//这个和NONE没有太大区别,只有图片非常大的时候才会有压缩的作用
scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
} else {
//获取当前载体的宽高
ImageSize targetSize = decodingInfo.getTargetSize();
//当前是否按照循环2除的方式获取压缩比例
boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
//压缩逻辑,具体看方法内部
scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
}
if (scale > 1 && loggingEnabled) {
L.d(LOG_SUBSAMPLE_IMAGE, imageSize, imageSize.scaleDown(scale), scale, decodingInfo.getImageKey());
}
Options decodingOptions = decodingInfo.getDecodingOptions();
//设置压缩大小
decodingOptions.inSampleSize = scale;
return decodingOptions;
}
/**
* 重置输入流,用于再次获取Bitmap
* 一般来说流用过之后要再次使用必须重置或者重新获取
*/
protected InputStream resetStream(InputStream imageStream, ImageDecodingInfo decodingInfo) throws IOException {
if (imageStream.markSupported()) {//当前流支持定位功能
try {
//重置流
imageStream.reset();
return imageStream;
} catch (IOException ignored) {
}
}
//如果当前流不支持定位功能,先关闭旧的流,再重新获取输入流
IoUtils.closeSilently(imageStream);
return getImageStream(decodingInfo);
}
protected Bitmap considerExactScaleAndOrientatiton(Bitmap subsampledBitmap, ImageDecodingInfo decodingInfo,
int rotation, boolean flipHorizontal) {
Matrix m = new Matrix();
ImageScaleType scaleType = decodingInfo.getImageScaleType();
//可以在ImageLoaderConfiguration或DisplayOptions中配置ImageScaleType
//如果为EXACTLY系列,要考虑拉伸或者压缩到target大小
if (scaleType == ImageScaleType.EXACTLY || scaleType == ImageScaleType.EXACTLY_STRETCHED) {
ImageSize srcSize = new ImageSize(subsampledBitmap.getWidth(), subsampledBitmap.getHeight(), rotation);
float scale = ImageSizeUtils.computeImageScale(srcSize, decodingInfo.getTargetSize(), decodingInfo
.getViewScaleType(), scaleType == ImageScaleType.EXACTLY_STRETCHED);
if (Float.compare(scale, 1f) != 0) {
m.setScale(scale, scale);
if (loggingEnabled) {
L.d(LOG_SCALE_IMAGE, srcSize, srcSize.scale(scale), scale, decodingInfo.getImageKey());
}
}
}
if (flipHorizontal) {//实际上就是每一个x坐标取-,看上去就是横向的镜像对称
m.postScale(-1, 1);
if (loggingEnabled) L.d(LOG_FLIP_IMAGE, decodingInfo.getImageKey());
}
if (rotation != 0) {//是否要旋转bitmap
m.postRotate(rotation);
if (loggingEnabled) L.d(LOG_ROTATE_IMAGE, rotation, decodingInfo.getImageKey());
}
//处理拉伸/压缩,旋转和颠倒,这个过程相对会耗费一些内存
Bitmap finalBitmap = Bitmap.createBitmap(subsampledBitmap, 0, 0, subsampledBitmap.getWidth(), subsampledBitmap
.getHeight(), m, true);
if (finalBitmap != subsampledBitmap) {
subsampledBitmap.recycle();//手动回收旧的bitmap,已经没用了
}
return finalBitmap;
}
大致可以将以上流程分为5步
1.获取图片的输入流
2.获得当前图片的宽高(不应该将bitmap载入内存)和应该旋转的角度
3.重置输入流
4.通过载体的大小计算压缩比例
5.根据之前计算的选择角度和压缩比例等获得最终的bitmap
接下来关心一下ImageLoader的压缩处理算法:
上面有提到,压缩处理与ImageScaleType相关,其中NONE是完全不压缩,NONE_SAFE是保证图片不大于默认的最大大小,基本上就是2048*2048.
重点看一下IN_SAMPLE_POWER_OF_2、IN_SAMPLE_INT、EXACTLY和EXACTLY_STRETCHED,这些比较常用一点。
public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
boolean powerOf2Scale) {
//获取Bitmap的原始宽高
final int srcWidth = srcSize.getWidth();
final int srcHeight = srcSize.getHeight();
//获取载体的宽高
final int targetWidth = targetSize.getWidth();
final int targetHeight = targetSize.getHeight();
int scale = 1;
//inSampleSize是用于压缩的,一般来说2的话就意味着宽和高都变成原来的一般,从像素和内存的角度就是变成了1/4
switch (viewScaleType) {
case FIT_INSIDE://对应于FIT_XY系列
if (powerOf2Scale) {//ImageScaleType.IN_SAMPLE_POWER_OF_2
final int halfWidth = srcWidth / 2;
final int halfHeight = srcHeight / 2;
//循环2除,直到bitmap的宽高都小于等于载体的宽高
//FIT系列在设置的时候会自动拉伸,所以说FIT_XY会有拉伸
//注意到这里如果视图没有绘制完成或者wrap_content,并且没有指定maxHeight/Width的时候调用
//此时target默认为屏幕,图片基本可以认为不会进行压缩
while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) {
scale *= 2;
}
} else {//IN_SAMPLE_INT、EXACTLY、EXACTLY_STRETCHED
//这种方式不一定会得到2的倍数,但是BitmapFactory的inSampleSize只支持2的倍数
//其中BitmapFactory会自动向下取,比方说这里取7,但是实际上用的是4,会比预期要大
scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight);
}
break;
case CROP://对应于CROP系列
if (powerOf2Scale) {
final int halfWidth = srcWidth / 2;
final int halfHeight = srcHeight / 2;
//只要有宽/高压缩到target的宽/高即可,就是说可能存在另一边大于target的情况
while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) {
scale *= 2;
}
} else {
//这种方式不一定会得到2的倍数,但是BitmapFactory的inSampleSize只支持2的倍数
//其中BitmapFactory会自动向下取,比方说这里取7,但是实际上用的是4
//这里预期是宽/高有一边到达target的宽/高即可,这样会导致可能两边都比target大
scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight);
}
break;
}
if (scale < 1) {//注意这里是没有拉伸操作的,当然inSampleSize也不支持<1,默认也会使用1
scale = 1;
}
//不允许大于2048x2048
scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);
//如果不采用2除的方式,inSampleSize默认会取1,2,4,8...,从而向下取整
return scale;
}
/**
* 这个方法只有EXACTLY和EXACTLY_STRETCHED会执行
**/
public static float computeImageScale(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
boolean stretch) {
//压缩后的bitmap的宽高
final int srcWidth = srcSize.getWidth();
final int srcHeight = srcSize.getHeight();
//载体的宽高
final int targetWidth = targetSize.getWidth();
final int targetHeight = targetSize.getHeight();
//
final float widthScale = (float) srcWidth / targetWidth;
final float heightScale = (float) srcHeight / targetHeight;
final int destWidth;
final int destHeight;
//将宽/高其中的靠近载体的一边变成target的大小,另一边按照比例拉伸或者压缩
if ((viewScaleType == ViewScaleType.FIT_INSIDE && widthScale >= heightScale) ||
(viewScaleType == ViewScaleType.CROP && widthScale < heightScale)) {
destWidth = targetWidth;
destHeight = (int) (srcHeight / widthScale);
} else {
destWidth = (int) (srcWidth / heightScale);
destHeight = targetHeight;
}
float scale = 1;
//EXACTLY的时候只会进行压缩
//FIT系列会按照最大边进行压缩,那么压缩完之后bitmap的宽高必然小于载体
//CROP系列会按照最小边进行压缩,那么压缩完是保证至少有一边满足载体要求
//EXACTLY_STRETCHED会进行压缩或者拉伸,压缩的表现同EXACTLY
//FIT系列会进行拉伸,知道有一边满足载体要求
//CROP系列在压缩的时候,基本上会保证bitmap大于等于载体,拉伸基本上没啥可能
//一般来说提供相同比例的图片,那么就是宽高都是完全匹配的,这两种模式没有任何区别
if ((!stretch && destWidth < srcWidth && destHeight < srcHeight) || (stretch && destWidth != srcWidth && destHeight != srcHeight)) {
scale = (float) destWidth / srcWidth;
}
return scale;
}
稍微总结一下:
首先是ImageScaleType对压缩的影响
FIX系列:保证宽高都必定小于等于载体的宽高。
CROP系列:保证宽/高有一边小于等于载体即可。
IN_SAMPLE_POWER_OF_2:遵循BitmapFactory的inSampleSize规则,每一次压缩必定是2的倍数。最后压出来的图片在展示的时候是依赖ImageView本身的ScaleType,比方说FITXY的时候,实际上bitmap是比ImageView小的,但是ImageView在展示的时候会拉伸bitmap,从而铺满ImageView。
IN_SAMPLE_INT:直接计算压缩比例,会导致计算的数字不满足2的倍数,那么BitmapFactory在处理的时候会向下取2的倍数,也就是说算出来是7,实际上压缩的时候用的是4。这样会导致压缩之后的bitmap偏大。最后展示的时候同样会依赖于ImageView的ScaleType。比起IN_SAMPLE_POWER_OF_2来说,个人认为相对会占用多一点的内存。
EXACTLY:压缩的算法使用的是IN_SAMPLE_INT,在IN_SAMPLE_INT的处理基础上,如果当前bitmap比载体还要大一些,会进行二次压缩。
FIT系列要求宽高都小于载体,CROP保证至少有一边等于载体。
如果当前bitmap比载体要小,不会拉伸bitmap。
后续的展示依赖于ImageView的ScaleType。
EXACTLY_STRETCHED:在IN_SAMPLE_INT的处理基础上,会压缩或者拉伸bitmap。
FIT系列要求宽高都小于载体,CROP保证至少有一边等于载体。
上述的所有处理都会保持bitmap原有的宽高比例。
总结
ImageLoader基本上是满足大部分的使用,个人觉得有两个比较明显的问题,一个是不支持gif。其次就是从硬盘和网络上获取图片的时候使用的是cache线程池,这样在图片很多的时候会导致CPU非常忙碌。