Volley源代码分析 – 4: Request实现之ImageRequest&ImageLoader

之前把Volley的源代码都分析了一边,基本上整体的思路已经清晰,现在再来分析一下一个具体的Request实现,即ImageRequest。为什么是分析ImageRequest而不是其他的JsonRequest之类。一方面是因为在Android应用开发中,图像的使用比较重要,而且需要小心处理,避免OOM的问题;另外一个原因,相比ImageRequest或者StringRequest,ImageRequest实现稍微复杂,且还有一个辅助的工具类ImageLoader。

ImageRequest

/**
 * A canned request for getting an image at a given URL and calling
 * back with a decoded Bitmap.
 * 一个封装的request,用来从指定的URL获取图片,并且回调中返回一个解码的Bitmap
 */
public class ImageRequest extends Request<Bitmap> {
  /** Socket timeout in milliseconds for image requests */
  /** Socket超时时间设置 */
    private static final int IMAGE_TIMEOUT_MS = 1000;

    /** Default number of retries for image requests */
    /** 默认重试次数 */
    private static final int IMAGE_MAX_RETRIES = 2;

    /** Default backoff multiplier for image requests */
    /** Backoff系数,个人理解的是,失败的次数越多,那么下次应该拖的时间也就越长 */
    private static final float IMAGE_BACKOFF_MULT = 2f;

    private final Response.Listener<Bitmap> mListener;
    private final Config mDecodeConfig;
    private final int mMaxWidth;
    private final int mMaxHeight;

    /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
    /** 锁,用来保证在同一个时刻仅仅有衣服图片用于解码,避免OOM */
    private static final Object sDecodeLock = new Object();

    /**
     * Creates a new image request, decoding to a maximum specified width and
     * height. If both width and height are zero, the image will be decoded to
     * its natural size. If one of the two is nonzero, that dimension will be
     * clamped and the other one will be set to preserve the image's aspect
     * ratio. If both width and height are nonzero, the image will be decoded to
     * be fit in the rectangle of dimensions width x height while keeping its
     * aspect ratio.
     * 
     * 创建一个新的image request,解码最大为现定的最大宽度和高度。如果两者都是0的话,图片会
     * 根据其本身的大小进行解码。如果其中一个为0,那么就会根据另外一个边从尺寸进行解码。如果两者
     * 都不是0,那么图片会在保持其比例的情况下缩放至最长的边也符合。
     *
     * @param url URL of the image
     * @param listener Listener to receive the decoded bitmap
     * @param maxWidth Maximum width to decode this bitmap to, or zero for none
     * @param maxHeight Maximum height to decode this bitmap to, or zero for
     *            none
     * @param decodeConfig Format to decode the bitmap to
     * @param errorListener Error listener, or null to ignore errors
     */
    public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
            Config decodeConfig, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        setRetryPolicy(
                new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
    }

    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

    /**
     * Scales one side of a rectangle to fit aspect ratio.
     * 根据其中的一条边求得合适的缩放比例
     *
     * @param maxPrimary Maximum size of the primary dimension (i.e. width for
     *        max width), or zero to maintain aspect ratio with secondary
     *        dimension
     * @param maxSecondary Maximum size of the secondary dimension, or zero to
     *        maintain aspect ratio with primary dimension
     * @param actualPrimary Actual size of the primary dimension
     * @param actualSecondary Actual size of the secondary dimension
     */
    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
            int actualSecondary) {
        // If no dominant value at all, just return the actual.
        if (maxPrimary == 0 && maxSecondary == 0) {
            return actualPrimary;
        }

        // If primary is unspecified, scale primary to match secondary's scaling ratio.
        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 (resized * ratio > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }

    /**
     * 覆写Request的parseNetworkResponse方法,用来解析网络上获取的二进制流为Bitmap图片
     */
    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        // Serialize all decode on a global lock to reduce concurrent heap usage.
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                return Response.error(new ParseError(e));
            }
        }
    }

    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     * parseNetworkResponse的核心部分,为了易读性放在外面。
     */
    private Response<Bitmap> doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap = null;
        // 判断如果其长度和宽度都指定为0的话,那么就按照原来的尺寸进行解码。
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            // If we have to resize this image, first get the natural bounds.
            // 如果我们需要对image进行缩放的话,首先通过设置inJustDecodeBounds为true,来获取图片真实的大小
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            // 计算我们实际上要缩放的大小。
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth);

            // Decode to the nearest power of two scaling factor.
            // 首先将图片缩放到最接近我们所需要的大小的合适的比例,该比例为2^x。
            decodeOptions.inJustDecodeBounds = false;
            // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
            // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
            decodeOptions.inSampleSize =
                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap =
                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // If necessary, scale down to the maximal acceptable size.
            // 如果需要的话,也就是还需要进一步缩放的话,将tempBitmap缩放到合适的大小。
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap,
                        desiredWidth, desiredHeight, true);
                // 回收不用的tempBitmap。
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

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

    /**
     * 覆写Response的deliverResponse方法,注意该方法是在main线程上运行的。
     */
    @Override
    protected void deliverResponse(Bitmap response) {
        mListener.onResponse(response);
    }

    /**
     * Returns the largest power-of-two divisor for use in downscaling a bitmap
     * that will not result in the scaling past the desired dimensions.
     * 寻找最接近所需要缩放尺寸的2^x缩放比例
     *
     * @param actualWidth Actual width of the bitmap
     * @param actualHeight Actual height of the bitmap
     * @param desiredWidth Desired width of the bitmap
     * @param desiredHeight Desired height of the bitmap
     */
    // Visible for testing.
    static int findBestSampleSize(
            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desiredHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }

        return (int) n;
    }
}

其实该部分的核心的代码是doParse(NetworkResponse response)部分,其工作为:

  1. 首先检查,如果不需要缩放,那么就直接解码
  2. 需要缩放,则按照以下步骤进行
    1. 首先获取图片真实的大小,通过设置BitmapFactory.Options的inJustDecodeBounds属性
    2. 计算我们图片根据边框限制,所需要实际缩放的大小
    3. 由于在decode的时候,可以设定inSamleSize来获取近似的大小,减少内存占用。
    4. 再次判断图片是否完全在给定的范围之内,如果不是,那么进一步缩放。

ImageLoader

ImageLoader是基于ImageRequest的一个工具类,在ImageLoader的基础上,根据图片访问的特点做了更多的优化。我们首先来分析其结构。

在ImageLoader当中有很多辅助类用于优化设计,我们先看一看这些都是用来做什么的:

ImageCache

public interface ImageCache {
    public Bitmap getBitmap(String url);
    public void putBitmap(String url, Bitmap bitmap);
}

图片消耗较大,因此必须使用Cache进行缓存,在系统资源紧张或者图片已经不需使用的时候,及时的释放内存并复用,以减少内存的消耗。在ImageLoader没有默认的Cache实现,需要用户提供。在Volley的User Guide中,有一个简单的LruBitmap的cache的实现,链接为:http://blog.happyhls.me/2014/11/14/volley-lesson-2-making-a-standard-request%EF%BC%88%E8%AF%91%EF%BC%89/ 。该Cache继承了Android提供的LRUCache,以后需要的话再单独分析。

ImageListerner

    /**
     * Interface for the response handlers on image requests.
     * 用来处理response的回调接口
     *
     * The call flow is this:
     * 1. Upon being  attached to a request, onResponse(response, true) will
     * be invoked to reflect any cached data that was already available. If the
     * data was available, response.getBitmap() will be non-null.
     * 1、如果和一个request绑定之后,onResponse(response, true)会在被调用,表明在cache中有对应的数据。
     * 如果数据是可用的,那么response.getBitmap()不是null。
     *
     * 2. After a network response returns, only one of the following cases will happen:
     *   - onResponse(response, false) will be called if the image was loaded.
     *   or
     *   - onErrorResponse will be called if there was an error loading the image.
     * 2、当network的数据返回的时候,仅仅会发生下面的其中一种情况:
     *   - onResponse(response, false)会在image加载之后调用
     *   - onErrorResponse会在出错的时候调用
     */
    public interface ImageListener extends ErrorListener {
        /**
         * Listens for non-error changes to the loading of the image request.
         *
         * @param response Holds all information pertaining to the request, as well
         * as the bitmap (if it is loaded).
         * @param isImmediate True if this was called during ImageLoader.get() variants.
         * This can be used to differentiate between a cached image loading and a network
         * image loading in order to, for example, run an animation to fade in network loaded
         * images.
         */
        public void onResponse(ImageContainer response, boolean isImmediate);
    }

注释中拥有比较详细的描述,主要是设定了一个回调的接口,根据isImmediate来决定其显示的图片是默认的还是结果处理得到的结果。

在ImageLoader当中有一个静态方法,用来获取一个ImageListener实现,我们来观察其代码:

    public static ImageListener getImageListener(final ImageView view,
            final int defaultImageResId, final int errorImageResId) {
        return new ImageListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                if (errorImageResId != 0) {
                    view.setImageResource(errorImageResId);
                }
            }

            @Override
            public void onResponse(ImageContainer response, boolean isImmediate) {
                if (response.getBitmap() != null) {
                    view.setImageBitmap(response.getBitmap());
                } else if (defaultImageResId != 0) {
                    view.setImageResource(defaultImageResId);
                }
            }
        };
    }

比较简单,不需要多说。

ImageContainer

    /**
     * Container object for all of the data surrounding an image request.
     * 一个容器,其中包含了围绕着image请求的所有的数据
     */
    public class ImageContainer {
        /**
         * The most relevant bitmap for the container. If the image was in cache, the
         * Holder to use for the final bitmap (the one that pairs to the requested URL).
         * 
         * 容器最相关的bitmap。如果图片是在cache中,那么容器会使用这个来指向最终的bitmap。
         */
        private Bitmap mBitmap;

        private final ImageListener mListener;

        /** The cache key that was associated with the request */
        private final String mCacheKey;

        /** The request URL that was specified */
        private final String mRequestUrl;

        /**
         * Constructs a BitmapContainer object.
         * @param bitmap The final bitmap (if it exists).
         * @param requestUrl The requested URL for this container.
         * @param cacheKey The cache key that identifies the requested URL for this container.
         */
        public ImageContainer(Bitmap bitmap, String requestUrl,
                String cacheKey, ImageListener listener) {
            mBitmap = bitmap;
            mRequestUrl = requestUrl;
            mCacheKey = cacheKey;
            mListener = listener;
        }

        /**
         * Releases interest in the in-flight request (and cancels it if no one else is listening).
         */
        public void cancelRequest() {
            if (mListener == null) {
                return;
            }

            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
            if (request != null) {
                //此时reqeust对应的已经开始在执行
              
                //判断是否需要取消整个任务,即同一个URL是否还有其他的地方在用
                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
                if (canceled) {
                    mInFlightRequests.remove(mCacheKey);
                }
            } else {
                // check to see if it is already batched for delivery.
                //此时尚未执行
                request = mBatchedResponses.get(mCacheKey);
                if (request != null) {
                    request.removeContainerAndCancelIfNecessary(this);
                    if (request.mContainers.size() == 0) {
                        mBatchedResponses.remove(mCacheKey);
                    }
                }
            }
        }

        /**
         * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
         */
        public Bitmap getBitmap() {
            return mBitmap;
        }

        /**
         * Returns the requested URL for this container.
         */
        public String getRequestUrl() {
            return mRequestUrl;
        }
    }

这是一个容器类,用来存储与图片访问相关的任务信息,那么这个时候大家可能就问了,在Volley中既然已经有了一个默认的实现ImgeRequest,那么为什么还需要再做一个ImageContainer来保存期相关的信息呢?通过读ImageLoader的源代码可以发现,其实Volley为了尽可能减少资源的消耗,会将图片请求进行检查,如果图片的地址是一致的,这里面对应的CacheKey是一样的话,那么就会合并为一个任务,然后再提交给ReqeustQueue去执行。当Network返回结果的时候,就会检查该CacheKey对应的多有的任务信息,依次派发出去。

这个类比较简单,主要存储了Bitmap,ImageListener,CacheKey,ReqeustUrl等。然后还有一个Cancel方法,其思路则是判断该任务是否已经有了请求的结果,如果有的话,那么就删除对应的结果;如果没有请求的结果,则从正在执行的任务队列中删除。

BatchedImageRequest

    /**
     * Wrapper class used to map a Request to the set of active ImageContainer objects that are
     * interested in its results.
     * 一个包装来,用来包装那些活跃的ImageContainer的集合。
     */
    private class BatchedImageRequest {
        /** The request being tracked */
        private final Request<?> mRequest;

        /** The result of the request being tracked by this item */
        private Bitmap mResponseBitmap;

        /** Error if one occurred for this response */
        private VolleyError mError;

        /** List of all of the active ImageContainers that are interested in the request */
        /** 该图片的所有的请求,注意此reqeust并不是Volley中的Reqeust,而是需要做的任务,使用的ImageContainer类来保存相关的信息 */
        private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();

        /**
         * Constructs a new BatchedImageRequest object
         * @param request The request being tracked
         * @param container The ImageContainer of the person who initiated the request.
         */
        public BatchedImageRequest(Request<?> request, ImageContainer container) {
            mRequest = request;
            mContainers.add(container);
        }

        /**
         * Set the error for this response
         */
        public void setError(VolleyError error) {
            mError = error;
        }

        /**
         * Get the error for this response
         */
        public VolleyError getError() {
            return mError;
        }

        /**
         * Adds another ImageContainer to the list of those interested in the results of
         * the request.
         */
        public void addContainer(ImageContainer container) {
            mContainers.add(container);
        }

        /**
         * Detatches the bitmap container from the request and cancels the request if no one is
         * left listening.
         * 从容器中删掉对应的任务,如果删掉之后,对应的任务列表为空,则可以取消该任务,否则返回false
         * @param container The container to remove from the list
         * @return True if the request was canceled, false otherwise.
         */
        public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
            mContainers.remove(container);
            if (mContainers.size() == 0) {
                mRequest.cancel();
                return true;
            }
            return false;
        }
    }

该类就是完成我们刚刚提到的合并任务的功能。其中有一个链表LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();用来保存所有的同样的请求对应的任务信息。

说到这里,我们刚刚一直说到cacheKey,那么cacheKey到底是怎么样计算的呢?如下:

    private static String getCacheKey(String url, int maxWidth, int maxHeight) {
        return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
                .append("#H").append(maxHeight).append(url).toString();

可以看到包含了两部分内容

  • URL
  • maxWidth,maxHeight

到这里的分析,我们还是一头雾水,ImageLoader到底从哪里开始工作呢?最简单的办法是先跟着代码的流程走,一段典型的ImageLoader的使用代码为:

    ImageView imageView = (ImageView) findViewById(R.id.imageview);
    VolleyLog.setTag(getPackageName());
    RequestQueue requestQueue = Volley.newRequestQueue(getApplicationContext());
    ImageLoader imageLoader = new ImageLoader(requestQueue, new LruBitmapCache(getApplicationContext()));
    imageLoader.get("http://www.baidu.com/img/bdlogo.png", imageLoader.getImageListener(imageView, R.drawable.ic_launcher, R.drawable.ic_launcher));

从上面我们可以看出,所有的事件的发起,都是从get()方法开始的,那我们继续分析get方法

    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        // only fulfill requests that were initiated from the main thread.
        // 仅仅去完成那些在主线程上发起调用的请求,因为最后需要设置ImageView,只能在UI线程,即主线程上操作。
        throwIfNotOnMainThread();

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

        // Try to look up the request in the cache of remote images.
        // 尝试从cache中查找对应的图片
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // Return the cached bitmap.
            // 如果有,那么就分配一个ImageContainer
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            // 设置相应的图片
            imageListener.onResponse(container, true);
            return container;
        }

        // The bitmap did not exist in the cache, fetch it!
        // 图片在cache中不存在,需要从网络上获取
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // Update the caller to let them know that they should use the default bitmap.
        // 让其显示默认的图片
        imageListener.onResponse(imageContainer, true);

        // Check to see if a request is already in-flight.
        // 检查是否有可以合并的请求,已经在处理当中
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // If it is, add this request to the list of listeners.
            // 如果有,则添加到相应的队列当中,并返回
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // The request is not already in flight. Send the new request to the network and
        // track it.
        // 发起一个新的Image请求
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey);

        //添加到RequestQueue当中
        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

画一下其原理图

 

ImageLoader-get

好了,从上面的流程图我们也可以看出:

  1. 我们判断该段代码是否工作在UI线程,如果不是工作在主线程,那么则会抛出异常并退出。
  2. 根据图片的URL地址和相关的大小,获取在ImageLoader内部使用的唯一的CacheKey
  3. 根据CacheKey去在ImageCache中查找是否有对应的图片,如果有的话,则将信息保存在ImageContainer当中,然后通过imageListener的回调,返回结果
  4. 在Cache中没有,则说明需要通过网络进行请求
    1. 先查找该CacheKey是否已经在之前发起过请求,也可能是别处同样的图片地址和大小,也在执行请求。
    2. 根据情况分别进行处理
      1. 如果已经在BatchedReqeust中存在,那么则直接将这一次的请求添加到正在处理的请求中就可以。
      2. 如果没有,那么则新建一个Volley的Reqeust,并添加到对应的RequestQueue当中。
  5. 最后返回对应的ImageContainer即可。

About: happyhls


发表评论

电子邮件地址不会被公开。 必填项已用*标注