[关闭]
@946898963 2018-05-18T12:14:03.000000Z 字数 9849 阅读 1102

Volley-ImageRequest的源码解析

Android控件跟框架 Android源码分析


建议先阅读:Volley-Request的源码解析

ImageRequest是Request的子类,ImageRequest的作用是请求url获取图片资源,内部将响应的数据转换为了Bitmap对象,并对图像的尺寸和缩放进行了一些设置。下面对他的源码进行解析。

成员变量

  1. public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;
  2. public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
  3. public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;

重试策略的相关参数,关于Volley的重试策略,建议阅读:Volley的请求重试策略相关源码分析

  1. private final Response.Listener<Bitmap> mListener;

创建ImageRequest时候,传入的自定义的Listener的子类,在Listener的onResponse的方法,对请求返回的Bitmap进行处理。

mListener.onResponse(Bitmap response)是在deliverResponse中被调用的,而deliverResponse方法则是在ExecutorDelivery中被调用的,这个调用是发生在主线程的,所以我们可以在onResponse方法中操作UI,详细过程建议阅读:Volley-ResponseDelivery及其实现类的源码解析

  1. private final Config mDecodeConfig;
  2. private final int mMaxWidth;
  3. private final int mMaxHeight;
  4. private ScaleType mScaleType;

加载Bitmap的时候的相关参数。

Config表示图片解码时使用的颜色模式,也就是图片中每个像素颜色的表示方式。

Config参数的可选值有四个,分别为ALPHA_8,RGB_565,ARGB_4444,ARGB_8888。它们的含义列举如下。

参数取值 含义
ALPHA_8 图片中每个像素用一个字节(8位)存储,该字节存储的是图片8位的透明度值
RGB_565 图片中每个像素用两个字节(16位)存储,两个字节中高5位表示红色通道,中间6位表示绿色通道,低5位表示蓝色通道
ARGB_4444 图片中每个像素用两个字节(16位)存储,Alpha,R,G,B四个通道每个通道用4位表示
ARGB_8888 图片中每个像素用四个字节(32位)存储,Alpha,R,G,B四个通道每个通道用8位表示
  1. private static final Object sDecodeLock = new Object();

解析图像的时候的锁对象,用来对解析图像的方法进行加锁,保证了同一个时刻,直解析一个图像,避免内存溢出。

构造方法

  1. public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
  2. ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) {
  3. super(Method.GET, url, errorListener);
  4. setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS,DEFAULT_IMAGE_MAX_RETRIES,
  5. DEFAULT_IMAGE_BACKOFF_MULT));
  6. mListener = listener;
  7. mDecodeConfig = decodeConfig;
  8. mMaxWidth = maxWidth;
  9. mMaxHeight = maxHeight;
  10. mScaleType = scaleType;
  11. }
  12. @Deprecated
  13. public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
  14. Config decodeConfig, Response.ErrorListener errorListener) {
  15. this(url, listener, maxWidth, maxHeight,
  16. ScaleType.CENTER_INSIDE, decodeConfig, errorListener);
  17. }

构造方法就是对成员变量进行赋值操作,参数较少的构造方法最终调用的是参数较多的构造方法,我们可以看到,当我们没有设置图像的缩放方式的时候,默认是使用ScaleType.CENTER_INSIDE。在最终的构造方法中,调用了setRetryPolicy方法,重新设置了超时充实策略,关于Volley中的重试策略,建议阅读Volley-ResponseDelivery及其实现类的源码解析

  1. setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS,DEFAULT_IMAGE_MAX_RETRIES, DEFAULT_IMAGE_BACKOFF_MULT));

图像解析的相关方法

parseNetworkResponse方法

  1. @Override
  2. protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
  3. // Serialize all decode on a global lock to reduce concurrent heap usage.
  4. synchronized (sDecodeLock) {
  5. try {
  6. return doParse(response);
  7. } catch (OutOfMemoryError e) {
  8. VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
  9. return Response.error(new ParseError(e));
  10. }
  11. }
  12. }

parseNetworkResponse方法会对NetworkResponse进行解析,返回我们需要的Bitmap类型的Response,如果我们想要定义自己的Request的话,就需要重写这个方法,将NetworkResponse解析成我们想要的类型的Response。这个方法是在NetworkDispatcher的run方法中被调用的,关于NetworkDispatcher,建议阅读:Volley-NetworkDispatcher源码解析

我们可以看到parseNetworkResponse方法,最终是调用的doParse(response)方法对NetworkResponse进行处理的,接下来我们看下doParse(response)方法的源码。

doParse方法

  1. private Response<Bitmap> doParse(NetworkResponse response) {
  2. byte[] data = response.data;
  3. BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
  4. Bitmap bitmap = null;
  5. if (mMaxWidth == 0 && mMaxHeight == 0) {
  6. decodeOptions.inPreferredConfig = mDecodeConfig;
  7. bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
  8. } else {
  9. // If we have to resize this image, first get the natural bounds.
  10. decodeOptions.inJustDecodeBounds = true;
  11. BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
  12. int actualWidth = decodeOptions.outWidth;
  13. int actualHeight = decodeOptions.outHeight;
  14. // Then compute the dimensions we would ideally like to decode to.
  15. int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
  16. actualWidth, actualHeight, mScaleType);
  17. int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
  18. actualHeight, actualWidth, mScaleType);
  19. // Decode to the nearest power of two scaling factor.
  20. decodeOptions.inJustDecodeBounds = false;
  21. // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
  22. // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
  23. decodeOptions.inSampleSize =
  24. findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
  25. Bitmap tempBitmap =
  26. BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
  27. // If necessary, scale down to the maximal acceptable size.
  28. if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
  29. tempBitmap.getHeight() > desiredHeight)) {
  30. bitmap = Bitmap.createScaledBitmap(tempBitmap,
  31. desiredWidth, desiredHeight, true);
  32. tempBitmap.recycle();
  33. } else {
  34. bitmap = tempBitmap;
  35. }
  36. }
  37. if (bitmap == null) {
  38. return Response.error(new ParseError(response));
  39. } else {
  40. return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
  41. }
  42. }

doParse方法比较长,我们分开来看。

  1. byte[] data = response.data;
  2. BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
  3. Bitmap bitmap = null;

取出请求返回的数据的字节数组,创建用于对图片加载过程进行设置的BitmapFactory.Options对象。

  1. if (mMaxWidth == 0 && mMaxHeight == 0) {
  2. decodeOptions.inPreferredConfig = mDecodeConfig;
  3. bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
  4. }
  5. else{
  6. ....
  7. }

这里是一个if-else语句,我们先看if语句中的内容,当我们没有设置最终的Bitmap的宽度和高度的时候(或者手动设置为0),mMaxWidth和mMaxHeight默认为0,这时候就会执行if语句中的代码。可以看到很简单,就是设置了图片的解码方式,然后调用BitmapFactory.decodeByteArray方法,解析字节数组得到Bitmap对象,注意这里是按照图片的原始的宽度和高度解析出来的Bitmap。接着往下看。

  1. if (bitmap == null) {
  2. return Response.error(new ParseError(response));
  3. } else {
  4. return Response.success(bitmap,HttpHeaderParser.parseCacheHeaders(response));
  5. }

最后就是利用我们解析出来的Bitmap对象和parseCacheHeaders方法解析NetworkResponse构建的 Cache.Entry对象,创建了一个Bitmap类型的Response,并将其返回。

关于parseCacheHeaders方法,建议阅读:Volley-HttpHeaderParser源码解析

接下来,我们再分开来看else语句中的内容。

  1. decodeOptions.inJustDecodeBounds = true;
  2. BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
  3. int actualWidth = decodeOptions.outWidth;
  4. int actualHeight = decodeOptions.outHeight;

代码很简单,就是利用BitmapFactory.decodeByteArray方法解析图片得到原始的宽度和高度。注意!我们看到在解析图片前,将BitmapFactory.Options的inJustDecodeBounds参数设置为了true,这是为了节省加载内存,将inJustDecodeBounds设置为true之后,并不会真正的去加载图片数据,而只是解析图片的宽度和高度。

  1. int desiredWidth = getResizedDimension(mMaxWidth,mMaxHeight,actualWidth,actualHeight,mScaleType);
  2. int desiredHeight = getResizedDimension(mMaxHeight,mMaxWidth,actualHeight,actualWidth,mScaleType);

代码也很简单,就是利用我们解析得到的图片的原始的宽度和高度和我们设置的图片的最大的宽度和高度,计算出一个最理想的宽度和高度。这里只需要知道getResizedDimension的作用即可,后续会对这个方法的代码进行分析。

  1. decodeOptions.inJustDecodeBounds = false;
  2. decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
  3. Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

代码也很简单,先inJustDecodeBounds设置为false,然后利用findBestSampleSize方法,计算出一个合理的采样率,并将其赋值给BitmapFactory.Options的inSampleSize参数,最后利用BitmapFactory.decodeByteArray方法,解析字节数据组得到Bitmap对象。

这里我们看到一个新的方法-findBestSampleSize,这个方法的作用就是根据图片的实际的的宽度,高度和理想的宽度,高度,计算出一个合理的采样率,来按需加载图片,这个方法的源码后续会进行解析。

这里先简单介绍下BitmapFactory.Options的inSampleSize参数,在介绍findBestSampleSize方法的时候会进行详细的介绍,这个参数代表着采样率的意思,我们通过给它设置不同的值(它的值应该总是为2的指数)来对图片的尺寸设定一个要达到的缩放效果。
比方说:当inSampleSize的值为2的时候,代表着图片的宽和高都相应地变成为原来的1/2,像素数就会变为原来的1/4,占有的内存也就会变为原来的1/4。

这样,else语句中的代码就解析完了,其实else语句中的代码做的事情很简单,就是根据我们设置的图片的最大的宽度和高度和图片的原始的宽度和高度,计算出一个采样率,然后按需加载图片,这样可以节省内存,避免加载的Bitmap过大,造成内存溢出。最后我们来看看,else语句中用到的两个工具方法。

getResizedDimension方法

  1. private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,int actualSecondary, ScaleType scaleType) {
  2. if ((maxPrimary == 0) && (maxSecondary == 0)) {
  3. return actualPrimary;
  4. }
  5. if (scaleType == ScaleType.FIT_XY) {
  6. if (maxPrimary == 0) {
  7. return actualPrimary;
  8. }
  9. return maxPrimary;
  10. }
  11. if (maxPrimary == 0) {
  12. double ratio = (double) maxSecondary / (double) actualSecondary;
  13. return (int) (actualPrimary * ratio);
  14. }
  15. if (maxSecondary == 0) {
  16. return maxPrimary;
  17. }
  18. double ratio = (double) actualSecondary / (double) actualPrimary;
  19. int resized = maxPrimary;
  20. if (scaleType == ScaleType.CENTER_CROP) {
  21. if ((resized * ratio) < maxSecondary) {
  22. resized = (int) (maxSecondary / ratio);
  23. }
  24. return resized;
  25. }
  26. if ((resized * ratio) > maxSecondary) {
  27. resized = (int) (maxSecondary / ratio);
  28. }
  29. return resized;
  30. }

这个方法做的事情就是根据某个规则(规则如何定义的,为什么这样,我也不知道),根据图片的实际的宽度,高度和我们设置的最大的宽度,高度,计算出一个最理想的宽度和高度。

规则是这样的:

  1. 如果宽或高之一不为0, 那么将按照这一值将对应的宽或高按比例缩放,也就是说为0的尺寸实际会按照不为0的尺寸进行等比例缩放;
  2. 如果宽和高都不为0, 那么将按照指定的宽和高解码, 但仍然会等比例缩放, 也就是说, 如果传入的宽高比和原图宽高比不一致, 会进行裁剪;

findBestSampleSize方法

在介绍这个方法之前,我们先详细的介绍下BitmapFactory.Options的inSampleSize参数。

当这个参数为1时,采样后的图片大小和原来一样;当这个参数为2时,采样后的图片宽高均为原来的1/2,大小也就成了原来的1/4。也就是说,采样后的大小等于原始大小除以采样率的平方。

官方文档规定,inSampleSize的值应为2的非负整数次幂(1,2,4,… ),否则会被系统向下取整并找到一个最接近的值。通过设置inSampleSize我们就能够将图片缩放到一个合理的大小,那么该如何设置inSampleSize的值呢?

先来考虑以下情况:我们的ImageView的大小为100x100,要显示的图片大小为200x200,此时我们应该将inSampleSize设为2即可,但是如果要显示的图片大小为300x400,此时我们应该将inSampleSize设为多少呢?

首先我们通过计算可以得到图片宽是ImageView的3倍,而图片高是ImageView的4倍。那么我们应该将图片宽高缩小为原来的4倍吗?假如我们把图片宽高都变为原来的1/4,那么现在图片大小为75x 100,ImageView大小为100x100,图片要显示在ImageView中需要进行拉伸,而拉伸的话可能会导致图片失真。所以我们应该把图片宽高变为原来的1/3,以保证它不小于ImageView的大小,这样尽管多占用一些内存,但不会造成图片质量的下降,这还是很有必要的。

通过以上分析,我们知道了在设置inSampleSize时应该注意使得缩放后的图片大小不小于相应的ImageView大小。

下面来看findBestSampleSize方法的实现:

  1. static int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
  2. double wr = (double) actualWidth / desiredWidth;
  3. double hr = (double) actualHeight / desiredHeight;
  4. double ratio = Math.min(wr, hr);
  5. float n = 1.0f;
  6. while ((n * 2) <= ratio) {
  7. n *= 2;
  8. }
  9. return (int) n;
  10. }

这个方法的逻辑很直接,这里就不分析了。关于计算采样率的方法,还有另一种写法:

  1. private static int calcuateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
  2. int width = options.outWidth;//注意这里width是以像素为单位的
  3. int height = options.outHeight;//注意这里height是以像素为单位的
  4. int inSampleSize = 1;
  5. if (height > reqHeight || width > reqWidth) {
  6. int halfHeight = height / 2;
  7. int halfWidth = width / 2;
  8. while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
  9. inSampleSize *= 2;
  10. }
  11. }
  12. return inSampleSize;
  13. }

其他方法

getPriority方法

  1. @Override
  2. public Priority getPriority() {
  3. return Priority.LOW;
  4. }

重写了Request的getPriority方法,设置ImageRequest的优先级为低优先级,之所以这样是因为图片的网络加载要比字符慢, 所以优先级设置为low,优先进行字符的加载。

deliverRespons方法

响应分发方法,调用了我们传递进去的Listener的onResponse方法,这个方法最终是在ExecutorDelivery中被调用的,这个调用是发生在主线程的,所以我们可以在onResponse方法中操作UI,详细过程建议阅读:Volley-ResponseDelivery及其实现类的源码解析

到这里ImageRequest的源码就解析完了,如有不对的地方,欢迎大家指出,感谢,撒欢。

建议阅读下面文章中和ImageRequest相关的部分:

Volley学习(三)ImageRequest、ImageLoader、NetworkImageView源码简读
Volley库源码解析

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注