Volley 源码解析(二)

图片加载


用过Volley图片加载的老司机们可能对这段代码非常的熟悉:

imageLoader.get( url,
                ImageLoader.getImageListener(iv, R.mipmap.aio_image_default, R.mipmap.aio_image_fail))

只要把图片地址、要显示的ImageView给到ImageLoader就可以自动帮你加载了,到底他是怎么实现的呢?我们一起到源码看看。



ImageLoader


1.ImageListener


ImageListener是图片加载结果的回调,它本身是一个接口,

    public interface ImageListener extends ErrorListener {

        public void onResponse(ImageContainer response, boolean isImmediate);

    }

    public interface ErrorListener {
        void onErrorResponse(VolleyError var1);
    }



里面有两个没有实现的方法,分别是成功和失败的回调。这很好理解,下面看看怎么获取它:

    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);
                }
            }
        };
    }



直接用了一个内部类把它给实现了,并且返回,我们只需要知道,当外界调取这个方法的时候,就获取了一个ImageListener对象。



2.异步执行

  public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        // only fulfill requests that were initiated from the main thread.
        throwIfNotOnMainThread();

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

        // Try to look up the request in the cache of remote images.
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // Return the cached bitmap.
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        // The bitmap did not exist in the cache, fetch it!
        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.
        Request<?> newRequest =
            new ImageRequest(requestUrl, new Listener<Bitmap>() {
                @Override
                public void onResponse(Bitmap response) {
                    onGetImageSuccess(cacheKey, response);
                }
            }, maxWidth, maxHeight,
            Config.RGB_565, new ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    onGetImageError(cacheKey, error);
                }
            });

        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

这段代码有点长,我们一步一步看。首先,必须要在主线程调用这个方法;第二步,根据图片的url,长度和宽度去获取缓存中的key,如果,缓存中有的话,直接执行返回。这里注意的一点是这个缓存对象,Volley并没有帮我们实现,是一个接口:

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

需要我们自己实现,就和Collections中的排序方法一样用了一种策略设计模式,可以自己自定义排序方式:

 public static <T> void sort(List<T> list, Comparator<? super T> c) {
        Object[] a = list.toArray();
        Arrays.sort(a, (Comparator)c);
        ListIterator i = list.listIterator();
        for (int j=0; j<a.length; j++) {
            i.next();
            i.set(a[j]);
        }
    }

我们这里也可以自己自定义缓存的具体实现;那么第三步,如果缓存中没有数据,那么先构建一个ImageContainer对象,并用imageListener对象去执行onResponse方法,让外界先用默认的图片显示;第四步,从mInFlightRequests对象获取正在执行的BatchedImageRequest,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 */
        private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();

很简单,不用详细说明了。至于mInFlightRequests这个对象和之前网络请求中的mWaitingRequests 对象非常相似,但又略有所不同。如果对网络请求源码不了解的,可以看我前面一篇Volley源码解析(一),同样,他也是为了防止同样的请求操作多次,但是他又把ImageListener给封装到了ImageContainer对象中去了。在详细看这段代码:

     // The bitmap did not exist in the cache, fetch it!
        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;
        }

为的是,当我们的同样的请求执行结束后,在用各自的ImageListener分发到各自的UI请求界面中,到后面代码就知道了。接下来第五步,如果缓存中不存在,请求又不是在航班上(执行中),那么我们就要去执行请求了:

    Request<?> newRequest =
            new ImageRequest(requestUrl, new Listener<Bitmap>() {
                @Override
                public void onResponse(Bitmap response) {
                    onGetImageSuccess(cacheKey, response);
                }
            }, maxWidth, maxHeight,
            Config.RGB_565, new ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    onGetImageError(cacheKey, error);
                }
            });

        mRequestQueue.add(newRequest);

就是使用了ImageRequest,至于请求结果我们稍后在看。最后一步所做的事情就是,将请求放到航班队列上,防止一个请求多次请求网络:

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



3.请求结果处理


请求成功或失败的代码都差不多,我们就看看执行成功了改做什么:

  private void onGetImageSuccess(String cacheKey, Bitmap response) {
        // cache the image that was fetched.
        mCache.putBitmap(cacheKey, response);

        // remove the request from the list of in-flight requests.
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            // Update the response bitmap.
            request.mResponseBitmap = response;

            // Send the batched response
            batchResponse(cacheKey, request);
        }
    }

首先,我们把得到的结果放入缓存中,然后把航班上的请求给移除了,表示请求执行结束。并把请求结果放到BatchedImageRequest对象中去。重点就是batchResponse方法了,点进去看看:

  private void batchResponse(String cacheKey, BatchedImageRequest request) {
        mBatchedResponses.put(cacheKey, request);
        // If we don't already have a batch delivery runnable in flight, make a new one.
        // Note that this will be used to deliver responses to all callers in mBatchedResponses.
        if (mRunnable == null) {
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    for (BatchedImageRequest bir : mBatchedResponses.values()) {
                        for (ImageContainer container : bir.mContainers) {
                            // If one of the callers in the batched request canceled the request
                            // after the response was received but before it was delivered,
                            // skip them.
                            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, mBatchResponseDelayMs);
        }
    }

到这里,想必很清楚了,把BatchedImageRequest中的结果分别用各自的ImageListener去执行返回结果。



NetworkImageView


NetworkImageView是继承ImageView,并且对ImageLoader的一个封装,比较简单。首先要把确认的图片和加载失败的图片给它:

 /** * Sets the default image resource ID to be used for this view until the attempt to load it * completes. */
    public void setDefaultImageResId(int defaultImage) {
        mDefaultImageId = defaultImage;
    }

    /** * Sets the error image resource ID to be used for this view in the event that the image * requested fails to load. */
    public void setErrorImageResId(int errorImage) {
        mErrorImageId = errorImage;
    }

当我们调用setImageUrl()方法的时候,就开始加载了。

    public void setImageUrl(String url, ImageLoader imageLoader) {
        mUrl = url;
        mImageLoader = imageLoader;
        // The URL has potentially changed. See if we need to load it.
        loadImageIfNecessary(false);
    }

主要代码在loadImageIfNecessary,我们点进去

  private void loadImageIfNecessary(final boolean isInLayoutPass) {
        int width = getWidth();
        int height = getHeight();

        boolean isFullyWrapContent = getLayoutParams() != null
                && getLayoutParams().height == LayoutParams.WRAP_CONTENT
                && getLayoutParams().width == LayoutParams.WRAP_CONTENT;
        // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
        // view, hold off on loading the image.
        if (width == 0 && height == 0 && !isFullyWrapContent) {
            return;
        }

        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
        // currently loaded image.
        if (TextUtils.isEmpty(mUrl)) {
            if (mImageContainer != null) {
                mImageContainer.cancelRequest();
                mImageContainer = null;
            }
            setDefaultImageOrNull();
            return;
        }

        // if there was an old request in this view, check if it needs to be canceled.
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                // if the request is from the same URL, return.
                return;
            } else {
                // if there is a pre-existing request, cancel it if it's fetching a different URL.
                mImageContainer.cancelRequest();
                setDefaultImageOrNull();
            }
        }

        // The pre-existing content of this view didn't match the current URL. Load the new image
        // from the network.
        ImageContainer newContainer = mImageLoader.get(mUrl,
                new ImageListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        if (mErrorImageId != 0) {
                            setImageResource(mErrorImageId);
                        }
                    }

                    @Override
                    public void onResponse(final ImageContainer response, boolean isImmediate) {
                        // If this was an immediate response that was delivered inside of a layout
                        // pass do not set the image immediately as it will trigger a requestLayout
                        // inside of a layout. Instead, defer setting the image by posting back to
                        // the main thread.
                        if (isImmediate && isInLayoutPass) {
                            post(new Runnable() {
                                @Override
                                public void run() {
                                    onResponse(response, false);
                                }
                            });
                            return;
                        }

                        if (response.getBitmap() != null) {
                            setImageBitmap(response.getBitmap());
                        } else if (mDefaultImageId != 0) {
                            setImageResource(mDefaultImageId);
                        }
                    }
                });

        // update the ImageContainer to be the new bitmap container.
        mImageContainer = newContainer;
    }

首先,如果获取不到NetworkImageView的长宽就return,什么都不做。然后如果传过来的url是空的,就放默认的图片到视图上。接下来这段代码也是一个缓存策略

     // if there was an old request in this view, check if it needs to be canceled.
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                // if the request is from the same URL, return.
                return;
            } else {
                // if there is a pre-existing request, cancel it if it's fetching a different URL. mImageContainer.cancelRequest(); setDefaultImageOrNull(); } }

至于我们的mImageContainer是它内部维护的一个对象,之前在看ImageLoader的时候已经看到它了,封装了执行结果,url,缓存key,ImageListener对象。那么我们调用了ImageLoader.get()的方法之后,会返回一个ImageContainer对象。好,继续看代码,如果mImageContainer中的请求url和传过来的url是一样的,那么就返回,不需要执行,因为结果都是一样的。否则,取消当前的请求,设置默认的图片,继续下面的操作,想都不用想,肯定是加载图片呗。

  // The pre-existing content of this view didn't match the current URL. Load the new image
        // from the network.
        ImageContainer newContainer = mImageLoader.get(mUrl,
                new ImageListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        if (mErrorImageId != 0) {
                            setImageResource(mErrorImageId);
                        }
                    }

                    @Override
                    public void onResponse(final ImageContainer response, boolean isImmediate) {
                        // If this was an immediate response that was delivered inside of a layout
                        // pass do not set the image immediately as it will trigger a requestLayout
                        // inside of a layout. Instead, defer setting the image by posting back to
                        // the main thread.
                        if (isImmediate && isInLayoutPass) {
                            post(new Runnable() {
                                @Override
                                public void run() {
                                    onResponse(response, false);
                                }
                            });
                            return;
                        }

                        if (response.getBitmap() != null) {
                            setImageBitmap(response.getBitmap());
                        } else if (mDefaultImageId != 0) {
                            setImageResource(mDefaultImageId);
                        }
                    }
                });

但是这里值得注意的地方就是onResponse内部的方法:

   if (isImmediate && isInLayoutPass) {
                            post(new Runnable() {
                                @Override
                                public void run() {
                                    onResponse(response, false);
                                }
                            });
                            return;
                        }

这个是啥意思呢,其实就是当布局重绘的时候,调用NetworkImageView的onLayout()方法的时候

   @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        loadImageIfNecessary(true);
    }

传入一个true进来,那么isInLayoutPass就是true了,如果满足要是立即要执行的条件时,就会执行里面的方法。想想也很简单么,因为,我们要执行完onDraw的时候,我们才可以给它设置图片麽,所以用Post放到MQ队列中,让其绘制完了后在调用onResponse方法,注意,此时,传入了一个false进来,那么就会执行底下的代码了。这个不需要我们的关心,一般,我们调用setImageUrl传进来的就是false。



总结

当我们使用ImageLoader去异步执行请求的时候,会返回给我们ImageContainer,我们在使用这个ImageContainer的时候,要进行非空判断,因为它的内部维护的Bitmap可能为空


NetworkImageView使用起来虽然是很方面,但是如果大量的列表实现就不推荐使用NetworkImageView了,会占用大量的内存空间。

你可能感兴趣的:(源码,Volley)