android volley 下载图片大小,Android Volley图片加载功能详解

Gituhb项目

Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.

为什么写这篇博客

本来文章是维护在github上的,但是我在分析ImageLoader源码过程中与到了一个问题,希望大家能帮助解答.

Volley获取网络图片

本来想分析Universal Image Loader的源码,但是发现Volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:

1. 获取网络图片的url.

2. 判断该url对应的图片是否有本地缓存.

3. 有本地缓存,直接使用本地缓存图片,通过异步回调给ImageView进行设置.

4. 无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给ImageView进行设置.

我们通过Volley源码,看一下Volley是否是按照这个步骤实现网络图片加载的.

ImageRequest.java

按照Volley的架构,我们首先需要构造一个网络图片请求,Volley帮我们封装了ImageRequest类,我们来看一下它的具体实现:

/** 网络图片请求类. */

@SuppressWarnings("unused")

public class ImageRequest extends Request {

/** 默认图片获取的超时时间(单位:毫秒) */

public static final int DEFAULT_IMAGE_REQUEST_MS = 1000;

/** 默认图片获取的重试次数. */

public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;

private final Response.Listener mListener;

private final Bitmap.Config mDecodeConfig;

private final int mMaxWidth;

private final int mMaxHeight;

private ImageView.ScaleType mScaleType;

/** Bitmap解析同步锁,保证同一时间只有一个Bitmap被load到内存进行解析,防止OOM. */

private static final Object sDecodeLock = new Object();

/**

* 构造一个网络图片请求.

* @param url 图片的url地址.

* @param listener 请求成功用户设置的回调接口.

* @param maxWidth 图片的最大宽度.

* @param maxHeight 图片的最大高度.

* @param scaleType 图片缩放类型.

* @param decodeConfig 解析bitmap的配置.

* @param errorListener 请求失败用户设置的回调接口.

*/

public ImageRequest(String url, Response.Listener listener, int maxWidth, int maxHeight,

ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,

Response.ErrorListener errorListener) {

super(Method.GET, url, errorListener);

mListener = listener;

mDecodeConfig = decodeConfig;

mMaxWidth = maxWidth;

mMaxHeight = maxHeight;

mScaleType = scaleType;

}

/** 设置网络图片请求的优先级. */

@Override

public Priority getPriority() {

return Priority.LOW;

}

@Override

protected Response parseNetworkResponse(NetworkResponse response) {

synchronized (sDecodeLock) {

try {

return doParse(response);

} catch (OutOfMemoryError e) {

return Response.error(new VolleyError(e));

}

}

}

private Response doParse(NetworkResponse response) {

byte[] data = response.data;

BitmapFactory.Options decodeOptions = new BitmapFactory.Options();

Bitmap bitmap;

if (mMaxWidth == 0 && mMaxHeight == 0) {

decodeOptions.inPreferredConfig = mDecodeConfig;

bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

} else {

// 获取网络图片的真实尺寸.

decodeOptions.inJustDecodeBounds = true;

BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

int actualWidth = decodeOptions.outWidth;

int actualHeight = decodeOptions.outHeight;

int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,

actualWidth, actualHeight, mScaleType);

int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight,

actualWidth, actualHeight, mScaleType);

decodeOptions.inJustDecodeBounds = false;

decodeOptions.inSampleSize =

findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);

Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||

tempBitmap.getHeight() > desireHeight)) {

bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true);

tempBitmap.recycle();

} else {

bitmap = tempBitmap;

}

}

if (bitmap == null) {

return Response.error(new VolleyError(response));

} else {

return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));

}

}

static int findBestSampleSize(

int actualWidth, int actualHeight, int desiredWidth, int desireHeight) {

double wr = (double) actualWidth / desiredWidth;

double hr = (double) actualHeight / desireHeight;

double ratio = Math.min(wr, hr);

float n = 1.0f;

while ((n * 2) <= ratio) {

n *= 2;

}

return (int) n;

}

/** 根据ImageView的ScaleType设置图片的大小. */

private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,

int actualSecondary, ImageView.ScaleType scaleType) {

// 如果没有设置ImageView的最大值,则直接返回网络图片的真实大小.

if ((maxPrimary == 0) && (maxSecondary == 0)) {

return actualPrimary;

}

// 如果ImageView的ScaleType为FIX_XY,则将其设置为图片最值.

if (scaleType == ImageView.ScaleType.FIT_XY) {

if (maxPrimary == 0) {

return actualPrimary;

}

return maxPrimary;

}

if (maxPrimary == 0) {

double ratio = (double)maxSecondary / (double)actualSecondary;

return (int)(actualPrimary * ratio);

}

if (maxSecondary == 0) {

return maxPrimary;

}

double ratio = (double) actualSecondary / (double) actualPrimary;

int resized = maxPrimary;

if (scaleType == ImageView.ScaleType.CENTER_CROP) {

if ((resized * ratio) < maxSecondary) {

resized = (int)(maxSecondary / ratio);

}

return resized;

}

if ((resized * ratio) > maxSecondary) {

resized = (int)(maxSecondary / ratio);

}

return resized;

}

@Override

protected void deliverResponse(Bitmap response) {

mListener.onResponse(response);

}

}

因为Volley本身框架已经实现了对网络请求的本地缓存,所以ImageRequest做的主要事情就是解析字节流为Bitmap,再解析过程中,通过静态变量保证每次只解析一个Bitmap防止OOM,使用ScaleType和用户设置的MaxWidth和MaxHeight来设置图片大小.

总体来说,ImageRequest的实现非常简单,这里不做过多的讲解.ImageRequest的缺陷在于:

1.需要用户进行过多的设置,包括图片的大小的最大值.

2.没有图片的内存缓存,因为Volley的缓存是基于Disk的缓存,有对象反序列化的过程.

ImageLoader.java

鉴于以上两个缺点,Volley又提供了一个更牛逼的ImageLoader类.其中,最关键的就是增加了内存缓存.

再讲解ImageLoader的源码之前,需要先介绍一下ImageLoader的使用方法.和之前的Request请求不同,ImageLoader并不是new出来直接扔给RequestQueue进行调度,它的使用方法大体分为4步:

•创建一个RequestQueue对象.

RequestQueue queue = Volley.newRequestQueue(context);

•创建一个ImageLoader对象.

ImageLoader构造函数接收两个参数,第一个是RequestQueue对象,第二个是ImageCache对象(也就是内存缓存类,我们先不给出具体实现,讲解完ImageLoader源码之后,我会提供一个利用LRU算法的ImageCache实现类)

ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() {

@Override

public void putBitmap(String url, Bitmap bitmap) {}

@Override

public Bitmap getBitmap(String url) { return null; }

});

•获取一个ImageListener对象.

ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);

•调用ImageLoader的get方法加载网络图片.

imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);

有了ImageLoader的使用方法,我们结合使用方法来看一下ImageLoader的源码:

@SuppressWarnings({"unused", "StringBufferReplaceableByString"})

public class ImageLoader {

/**

* 关联用来调用ImageLoader的RequestQueue.

*/

private final RequestQueue mRequestQueue;

/** 图片内存缓存接口实现类. */

private final ImageCache mCache;

/** 存储同一时间执行的相同CacheKey的BatchedImageRequest集合. */

private final HashMap mInFlightRequests =

new HashMap();

private final HashMap mBatchedResponses =

new HashMap();

/** 获取主线程的Handler. */

private final Handler mHandler = new Handler(Looper.getMainLooper());

private Runnable mRunnable;

/** 定义图片K1缓存接口,即将图片的内存缓存工作交给用户来实现. */

public interface ImageCache {

Bitmap getBitmap(String url);

void putBitmap(String url, Bitmap bitmap);

}

/** 构造一个ImageLoader. */

public ImageLoader(RequestQueue queue, ImageCache imageCache) {

mRequestQueue = queue;

mCache = imageCache;

}

/** 构造网络图片请求成功和失败的回调接口. */

public static ImageListener getImageListener(final ImageView view, final int defaultImageResId,

final int errorImageResId) {

return new ImageListener() {

@Override

public void onResponse(ImageContainer response, boolean isImmediate) {

if (response.getBitmap() != null) {

view.setImageBitmap(response.getBitmap());

} else if (defaultImageResId != 0) {

view.setImageResource(defaultImageResId);

}

}

@Override

public void onErrorResponse(VolleyError error) {

if (errorImageResId != 0) {

view.setImageResource(errorImageResId);

}

}

};

}

public ImageContainer get(String requestUrl, ImageListener imageListener,

int maxWidth, int maxHeight, ScaleType scaleType) {

// 判断当前方法是否在UI线程中执行.如果不是,则抛出异常.

throwIfNotOnMainThread();

final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

// 从L1级缓存中根据key获取对应的Bitmap.

Bitmap cacheBitmap = mCache.getBitmap(cacheKey);

if (cacheBitmap != null) {

// L1缓存命中,通过缓存命中的Bitmap构造ImageContainer,并调用imageListener的响应成功接口.

ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null);

// 注意:因为目前是在UI线程中,因此这里是调用onResponse方法,并非回调.

imageListener.onResponse(container, true);

return container;

}

ImageContainer imageContainer =

new ImageContainer(null, requestUrl, cacheKey, imageListener);

// L1缓存命中失败,则先需要对ImageView设置默认图片.然后通过子线程拉取网络图片,进行显示.

imageListener.onResponse(imageContainer, true);

// 检查cacheKey对应的ImageRequest请求是否正在运行.

BatchedImageRequest request = mInFlightRequests.get(cacheKey);

if (request != null) {

// 相同的ImageRequest正在运行,不需要同时运行相同的ImageRequest.

// 只需要将其对应的ImageContainer加入到BatchedImageRequest的mContainers集合中.

// 当正在执行的ImageRequest结束后,会查看当前有多少正在阻塞的ImageRequest,

// 然后对其mContainers集合进行回调.

request.addContainer(imageContainer);

return imageContainer;

}

// L1缓存没命中,还是需要构造ImageRequest,通过RequestQueue的调度来获取网络图片

// 获取方法可能是:L2缓存(ps:Disk缓存)或者HTTP网络请求.

Request newRequest =

makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);

mRequestQueue.add(newRequest);

mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));

return imageContainer;

}

/** 构造L1缓存的key值. */

private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {

return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)

.append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)

.toString();

}

public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {

return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);

}

private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {

throwIfNotOnMainThread();

String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

return mCache.getBitmap(cacheKey) != null;

}

/** 当L1缓存没有命中时,构造ImageRequest,通过ImageRequest和RequestQueue获取图片. */

protected Request makeImageRequest(final String requestUrl, int maxWidth, int maxHeight,

ScaleType scaleType, final String cacheKey) {

return new ImageRequest(requestUrl, new Response.Listener() {

@Override

public void onResponse(Bitmap response) {

onGetImageSuccess(cacheKey, response);

}

}, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {

@Override

public void onErrorResponse(VolleyError error) {

onGetImageError(cacheKey, error);

}

});

}

/** 图片请求失败回调.运行在UI线程中. */

private void onGetImageError(String cacheKey, VolleyError error) {

BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

if (request != null) {

request.setError(error);

batchResponse(cacheKey, request);

}

}

/** 图片请求成功回调.运行在UI线程中. */

protected void onGetImageSuccess(String cacheKey, Bitmap response) {

// 增加L1缓存的键值对.

mCache.putBitmap(cacheKey, response);

// 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.

BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

if (request != null) {

request.mResponseBitmap = response;

// 将阻塞的ImageRequest进行结果分发.

batchResponse(cacheKey, request);

}

}

private void batchResponse(String cacheKey, BatchedImageRequest request) {

mBatchedResponses.put(cacheKey, request);

if (mRunnable == null) {

mRunnable = new Runnable() {

@Override

public void run() {

for (BatchedImageRequest bir : mBatchedResponses.values()) {

for (ImageContainer container : bir.mContainers) {

if (container.mListener == null) {

continue;

}

if (bir.getError() == null) {

container.mBitmap = bir.mResponseBitmap;

container.mListener.onResponse(container, false);

} else {

container.mListener.onErrorResponse(bir.getError());

}

}

}

mBatchedResponses.clear();

mRunnable = null;

}

};

// Post the runnable

mHandler.postDelayed(mRunnable, 100);

}

}

private void throwIfNotOnMainThread() {

if (Looper.myLooper() != Looper.getMainLooper()) {

throw new IllegalStateException("ImageLoader must be invoked from the main thread.");

}

}

/** 抽象出请求成功和失败的回调接口.默认可以使用Volley提供的ImageListener. */

public interface ImageListener extends Response.ErrorListener {

void onResponse(ImageContainer response, boolean isImmediate);

}

/** 网络图片请求的承载对象. */

public class ImageContainer {

/** ImageView需要加载的Bitmap. */

private Bitmap mBitmap;

/** L1缓存的key */

private final String mCacheKey;

/** ImageRequest请求的url. */

private final String mRequestUrl;

/** 图片请求成功或失败的回调接口类. */

private final ImageListener mListener;

public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey,

ImageListener listener) {

mBitmap = bitmap;

mRequestUrl = requestUrl;

mCacheKey = cacheKey;

mListener = listener;

}

public void cancelRequest() {

if (mListener == null) {

return;

}

BatchedImageRequest request = mInFlightRequests.get(mCacheKey);

if (request != null) {

boolean canceled = request.removeContainerAndCancelIfNecessary(this);

if (canceled) {

mInFlightRequests.remove(mCacheKey);

}

} else {

request = mBatchedResponses.get(mCacheKey);

if (request != null) {

request.removeContainerAndCancelIfNecessary(this);

if (request.mContainers.size() == 0) {

mBatchedResponses.remove(mCacheKey);

}

}

}

}

public Bitmap getBitmap() {

return mBitmap;

}

public String getRequestUrl() {

return mRequestUrl;

}

}

/**

* CacheKey相同的ImageRequest请求抽象类.

* 判定两个ImageRequest相同包括:

* 1. url相同.

* 2. maxWidth和maxHeight相同.

* 3. 显示的scaleType相同.

* 同一时间可能有多个相同CacheKey的ImageRequest请求,由于需要返回的Bitmap都一样,所以用BatchedImageRequest

* 来实现该功能.同一时间相同CacheKey的ImageRequest只能有一个.

* 为什么不使用RequestQueue的mWaitingRequestQueue来实现该功能?

* 答:是因为仅靠URL是没法判断两个ImageRequest相等的.

*/

private class BatchedImageRequest {

/** 对应的ImageRequest请求. */

private final Request> mRequest;

/** 请求结果的Bitmap对象. */

private Bitmap mResponseBitmap;

/** ImageRequest的错误. */

private VolleyError mError;

/** 所有相同ImageRequest请求结果的封装集合. */

private final LinkedList mContainers = new LinkedList();

public BatchedImageRequest(Request> request, ImageContainer container) {

mRequest = request;

mContainers.add(container);

}

public VolleyError getError() {

return mError;

}

public void setError(VolleyError error) {

mError = error;

}

public void addContainer(ImageContainer container) {

mContainers.add(container);

}

public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {

mContainers.remove(container);

if (mContainers.size() == 0) {

mRequest.cancel();

return true;

}

return false;

}

}

}

重大疑问

个人对Imageloader的源码有两个重大疑问?

•batchResponse方法的实现.

我很奇怪,为什么ImageLoader类里面要有一个HashMap来保存BatchedImageRequest集合呢?

private final HashMap mBatchedResponses =

new HashMap();

毕竟batchResponse是在特定的ImageRequest执行成功的回调中被调用的,调用代码如下:

protected void onGetImageSuccess(String cacheKey, Bitmap response) {

// 增加L1缓存的键值对.

mCache.putBitmap(cacheKey, response);

// 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.

BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

if (request != null) {

request.mResponseBitmap = response;

// 将阻塞的ImageRequest进行结果分发.

batchResponse(cacheKey, request);

}

}

从上述代码可以看出,ImageRequest请求成功后,已经从mInFlightRequests中获取了对应的BatchedImageRequest对象.而同一时间被阻塞的相同的ImageRequest对应的ImageContainer都在BatchedImageRequest的mContainers集合中.

那我认为,batchResponse方法只需要遍历对应BatchedImageRequest的mContainers集合即可.

但是,ImageLoader源码中,我认为多余的构造了一个HashMap对象mBatchedResponses来保存BatchedImageRequest集合,然后在batchResponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.

诡异代码如下:

private void batchResponse(String cacheKey, BatchedImageRequest request) {

mBatchedResponses.put(cacheKey, request);

if (mRunnable == null) {

mRunnable = new Runnable() {

@Override

public void run() {

for (BatchedImageRequest bir : mBatchedResponses.values()) {

for (ImageContainer container : bir.mContainers) {

if (container.mListener == null) {

continue;

}

if (bir.getError() == null) {

container.mBitmap = bir.mResponseBitmap;

container.mListener.onResponse(container, false);

} else {

container.mListener.onErrorResponse(bir.getError());

}

}

}

mBatchedResponses.clear();

mRunnable = null;

}

};

// Post the runnable

mHandler.postDelayed(mRunnable, 100);

}

}

我认为的代码实现应该是:

private void batchResponse(String cacheKey, BatchedImageRequest request) {

if (mRunnable == null) {

mRunnable = new Runnable() {

@Override

public void run() {

for (ImageContainer container : request.mContainers) {

if (container.mListener == null) {

continue;

}

if (request.getError() == null) {

container.mBitmap = request.mResponseBitmap;

container.mListener.onResponse(container, false);

} else {

container.mListener.onErrorResponse(request.getError());

}

}

mRunnable = null;

}

};

// Post the runnable

mHandler.postDelayed(mRunnable, 100);

}

}

•使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了.

自定义L1缓存类

首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.

实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:

import android.graphics.Bitmap;

import android.support.v4.util.LruCache;

/** Lru算法的L1缓存实现类. */

@SuppressWarnings("unused")

public class ImageLruCache implements ImageLoader.ImageCache {

private LruCache mLruCache;

public ImageLruCache() {

this((int) Runtime.getRuntime().maxMemory() / 8);

}

public ImageLruCache(final int cacheSize) {

createLruCache(cacheSize);

}

private void createLruCache(final int cacheSize) {

mLruCache = new LruCache(cacheSize) {

@Override

protected int sizeOf(String key, Bitmap value) {

return value.getRowBytes() * value.getHeight();

}

};

}

@Override

public Bitmap getBitmap(String url) {

return mLruCache.get(url);

}

@Override

public void putBitmap(String url, Bitmap bitmap) {

mLruCache.put(url, bitmap);

}

}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值