Volley源码分析(2)----ImageLoader

一:imageLoader

先来看看如何使用imageloader:

public void showImg(View view){
    ImageView imageView = (ImageView)this.findViewById(R.id.image_view);
    RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); 
         
    ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
    ImageListener listener = ImageLoader.getImageListener(imageView,R.drawable.default_image, R.drawable.default_image);
    imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener);
    //指定图片允许的最大宽度和高度
    //imageLoader.get("http://developer.android.com/images/home/aw_dac.png",listener, 200, 200);
}

ImageLoader操作挺繁琐,但是关键的就一句:

imageLoader.get("http://developer.android.com/images/home/aw_dac.png", listener);

下载图片。

所以我们来分析这句,至于BitmapCache,RequestQueue的作用和目的,在

Volley源码分析(1)----Volley 队列 中已经说明。

 

/**
     * Issues a bitmap request with the given URL if that image is not available
     * in the cache, and returns a bitmap container that contains all of the data
     * relating to the request (as well as the default image if the requested
     * image is not available).
     * @param requestUrl The url of the remote image
     * @param imageListener The listener to call when the remote image is loaded
     * @param maxWidth The maximum width of the returned image.
     * @param maxHeight The maximum height of the returned image.
     * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
     * @return A container object that contains all of the properties of the request, as well as
     *     the currently available image (default if remote is not loaded).
     */
    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        // only fulfill requests that were initiated from the main thread.
        throwIfNotOnMainThread();

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

        // 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<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);

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

确保该申请是在主线程进行的。

如果有cache,就直接返回结果:

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

判断是否有相同的请求正在队列里面,这里和前面volley的缓存十分类似!

最后添加一个imageRequest,放入队列里面。

关于cache的部分.

首先,

imageloader需要传入一个cache的对象,用以存放图片cache,我们称为image cache。

而网络请求队列也放置一个cache,放置网络返回的数据,我们称为request cache.

 

二:NetworkImageView

public class NetworkImageView extends ImageView 

可以直接使用的基于URL的imageView。

        networkImageView = (NetworkImageView) findViewById(R.id.nivTestView);
        
        mQueue = Volley.newRequestQueue(this);
        
        LruImageCache lruImageCache = LruImageCache.instance();
        
        ImageLoader imageLoader = new ImageLoader(mQueue,lruImageCache);
                
        networkImageView.setDefaultImageResId(R.drawable.ic_launcher);
        networkImageView.setErrorImageResId(R.drawable.ic_launcher);        
        networkImageView.setImageUrl(URLS[1], imageLoader);

使用非常简单,setImageUrl。

networkImageView极有可能时使用在listView 或者GridView等有大量使用的地方。

这里就有2个问题:

1.networkImageView需要在UI线程显示图片,图片的获取无疑是在后台线程的。

2.imageLoader是否可以重用?

我们先看第一个问题:

图片的加载更新在如下方法中,

void loadImageIfNecessary(final boolean isInLayoutPass)

如果URL为空,显示默认图片。

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

查看是否有old request在container中。

这个处理非常有用,在adapterlistView中,往往只是更新局部的内容,而这样就不需要处理,没有必要更新的imageView。

下面的过程就是调用imageloader的过程。

 

/**
     * Loads the image for the view if it isn't already loaded.
     * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
     */
    void loadImageIfNecessary(final boolean isInLayoutPass) {
        int width = getWidth();
        int height = getHeight();
        ScaleType scaleType = getScaleType();

        boolean wrapWidth = false, wrapHeight = false;
        if (getLayoutParams() != null) {
            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
            wrapHeight = getLayoutParams().height == 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.
        boolean isFullyWrapContent = wrapWidth && wrapHeight;
        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();
            }
        }

        // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
        int maxWidth = wrapWidth ? 0 : width;
        int maxHeight = wrapHeight ? 0 : height;

        // 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);
                        }
                    }
                }, maxWidth, maxHeight, scaleType);

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

2.imageLoader是否可以重用?

imageloader作为一个功能处理类,显然时不会跟一个imageview绑定在一起的。

我们更可以从上述分析的get方法中确定,每次get方法,如果需要都会生成一个imageRequest,所以并不会冲突。

 

三:Picasso分析

    Picasso不仅实现了图片异步加载的功能,还解决了android中加载图片时需要解决的一些常见问题:

   1.在adapter中需要取消已经不在视野范围的ImageView图片资源的加载,否则会导致图片错位,Picasso已经解决了这个问题。

   2.使用复杂的图片压缩转换来尽可能的减少内存消耗

   3.自带内存和硬盘二级缓存功能

我们首先来看看Picasso如何加载一个图片。

 首先它有几个加载的方法:

  /**
   * Start an image request using the specified URI.
   * <p>
   * Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,
   * if one is specified.
   *
   * @see #load(File)
   * @see #load(String)
   * @see #load(int)
   */
  public RequestCreator load(Uri uri) {
    return new RequestCreator(this, uri, 0);
  }

可以看到,它可以加载 文件,文件路径,资源id,当然还有url。

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

我们看看into方法:

com/squareup/picasso/RequestCreator.java

 /**
   * Asynchronously fulfills the request into the specified {@link ImageView} and invokes the
   * target {@link Callback} if it's not {@code null}.
   * <p>
   * <em>Note:</em> The {@link Callback} param is a strong reference and will prevent your
   * {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If
   * you use this method, it is <b>strongly</b> recommended you invoke an adjacent
   * {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.
   */
  public void into(ImageView target, Callback callback) {
    long started = System.nanoTime();
    checkMain();

    if (target == null) {
      throw new IllegalArgumentException("Target must not be null.");
    }

    if (!data.hasImage()) {
      picasso.cancelRequest(target);
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }

    if (deferred) {
      if (data.hasSize()) {
        throw new IllegalStateException("Fit cannot be used with resize.");
      }
      int width = target.getWidth();
      int height = target.getHeight();
      if (width == 0 || height == 0) {
        if (setPlaceholder) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;
      }
      data.resize(width, height);
    }

    Request request = createRequest(started);
    String requestKey = createKey(request);

    if (!skipMemoryCache) {
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {
        picasso.cancelRequest(target);
        setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
        if (picasso.loggingEnabled) {
          log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
        }
        if (callback != null) {
          callback.onSuccess();
        }
        return;
      }
    }

    if (setPlaceholder) {
      setPlaceholder(target, getPlaceholderDrawable());
    }

    Action action =
        new ImageViewAction(picasso, target, request, skipMemoryCache, noFade, errorResId,
            errorDrawable, requestKey, tag, callback);

    picasso.enqueueAndSubmit(action);
  }
into

一开始也是判断主线程,和Volley的networkImageView相同。

判断传入的uri,或者resourceid是否有问题。

不对,就取消这次请求。

下面就是调整图片请求的大小。

    if (deferred) {
      if (data.hasSize()) {
        throw new IllegalStateException("Fit cannot be used with resize.");
      }
      int width = target.getWidth();
      int height = target.getHeight();
      if (width == 0 || height == 0) {
        if (setPlaceholder) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;
      }
      data.resize(width, height);
    }

这段不重要,我们继续看下去:

    Request request = createRequest(started);
    String requestKey = createKey(request);
 /** Create the request optionally passing it through the request transformer. */
  private Request createRequest(long started) {
    int id = getRequestId();

    Request request = data.build();
    request.id = id;
    request.started = started;

    boolean loggingEnabled = picasso.loggingEnabled;
    if (loggingEnabled) {
      log(OWNER_MAIN, VERB_CREATED, request.plainId(), request.toString());
    }

    Request transformed = picasso.transformRequest(request);
    if (transformed != request) {
      // If the request was changed, copy over the id and timestamp from the original.
      transformed.id = id;
      transformed.started = started;

      if (loggingEnabled) {
        log(OWNER_MAIN, VERB_CHANGED, transformed.logId(), "into " + transformed);
      }
    }

    return transformed;
  }
createRequest

首先是配置id,和starttime,这个可以定位request。

requestKey其实就是一套方法用于区分request,更重要的是,获取缓存。

默认使用的memory cache 为LruCache。

此处可以imageLoader(指Volley里面的imageloader,下同)做比较,imageLoader一般使用BitmapCache

作为内存缓存,文件缓存有vollley requestQueue做处理。所以从结构上来说,Picasso和imageLoader是相似的。

接下来就是从LruCache中获取图片。

    if (!skipMemoryCache) {
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {
        picasso.cancelRequest(target);
        target.onBitmapLoaded(bitmap, MEMORY);
        return;
      }
    }

 

不需考虑,Picasso肯定也有请求队列,他是通过android的handle-thread的方式来驱动的。

  void submit(Action action) {
    dispatcher.dispatchSubmit(action);
  }
  void dispatchSubmit(Action action) {
    handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
  }
    @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        case REQUEST_SUBMIT: {
          Action action = (Action) msg.obj;
          dispatcher.performSubmit(action);
          break;
        }

该handler是在异步线程里面处理消息。

然后dispatcher.performSubmit(action);会在线程池中PicassoExecutorService启动task。

接着我们看com/squareup/picasso/BitmapHunter.java:

网络请求的,应该在这里处理。

Bitmap hunt() throws IOException {
    Bitmap bitmap = null;

    if (!skipMemoryCache) {
      bitmap = cache.get(key);
      if (bitmap != null) {
        stats.dispatchCacheHit();
        loadedFrom = MEMORY;
        if (picasso.loggingEnabled) {
          log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
        }
        return bitmap;
      }
    }

    data.loadFromLocalCacheOnly = (retryCount == 0);
    RequestHandler.Result result = requestHandler.load(data);
    if (result != null) {
      bitmap = result.getBitmap();
      loadedFrom = result.getLoadedFrom();
      exifRotation = result.getExifOrientation();
    }

    if (bitmap != null) {
      if (picasso.loggingEnabled) {
        log(OWNER_HUNTER, VERB_DECODED, data.logId());
      }
      stats.dispatchBitmapDecoded(bitmap);
      if (data.needsTransformation() || exifRotation != 0) {
        synchronized (DECODE_LOCK) {
          if (data.needsMatrixTransform() || exifRotation != 0) {
            bitmap = transformResult(data, bitmap, exifRotation);
            if (picasso.loggingEnabled) {
              log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
            }
          }
          if (data.hasCustomTransformations()) {
            bitmap = applyCustomTransformations(data.transformations, bitmap);
            if (picasso.loggingEnabled) {
              log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
            }
          }
        }
        if (bitmap != null) {
          stats.dispatchBitmapTransformed(bitmap);
        }
      }
    }

    return bitmap;
  }

开始还是从memory cache中获取。

在hunt方法中,

    RequestHandler.Result result = requestHandler.load(data);

这句就是图片获取的过程。

但是这个requestHandler究竟是什么?

从头看,在Picasso构造函数里面有:

    allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
    allRequestHandlers.add(new MediaStoreRequestHandler(context));
    allRequestHandlers.add(new ContentStreamRequestHandler(context));
    allRequestHandlers.add(new AssetRequestHandler(context));
    allRequestHandlers.add(new FileRequestHandler(context));
    allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));

所以,requestHandler可能就是上面一个里面的一个。

我们就看FileRequestHandler。

  @Override public boolean canHandleRequest(Request data) {
    return SCHEME_FILE.equals(data.uri.getScheme());
  }

也就是,FileRequestHandler只处理file类型的请求。

所以从网络获取图片com/squareup/picasso/NetworkRequestHandler.java:

直接看load:

@Override public Result load(Request data) throws IOException {
    Response response = downloader.load(data.uri, data.loadFromLocalCacheOnly);
    if (response == null) {
      return null;
    }

    Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK;

    Bitmap bitmap = response.getBitmap();
    if (bitmap != null) {
      return new Result(bitmap, loadedFrom);
    }

    InputStream is = response.getInputStream();
    if (is == null) {
      return null;
    }
    // Sometimes response content length is zero when requests are being replayed. Haven't found
    // root cause to this but retrying the request seems safe to do so.
    if (response.getContentLength() == 0) {
      Utils.closeQuietly(is);
      throw new IOException("Received response with 0 content-length header.");
    }
    if (loadedFrom == NETWORK && response.getContentLength() > 0) {
      stats.dispatchDownloadFinished(response.getContentLength());
    }
    try {
      return new Result(decodeStream(is, data), loadedFrom);
    } finally {
      Utils.closeQuietly(is);
    }
  }
load
      if (downloader == null) {
        downloader = Utils.createDefaultDownloader(context);
      }

download是在build里面创建的,其实很多内容都是在这里面创建的。

 public Picasso build()

最终获取的downloader对象是HttpURLConnection来进行网络通信。

我们看到response的结果是2种,DISK : NETWORK。

所以我们分析如何得到这2种结果,整个picasso的图片加载过程也就结束了。

1.UrlConnectionDownloader:

 @Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
      installCacheIfNeeded(context);
    }

    HttpURLConnection connection = openConnection(uri);
    connection.setUseCaches(true);
    if (localCacheOnly) {
      connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
    }

    int responseCode = connection.getResponseCode();
    if (responseCode >= 300) {
      connection.disconnect();
      throw new ResponseException(responseCode + " " + connection.getResponseMessage());
    }

    long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
    boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));

    return new Response(connection.getInputStream(), fromCache, contentLength);
  }

通过HttpUrlConnection的cache,来得到结果,以及fromCache。

2.OkHttpDownloader

@Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
    HttpURLConnection connection = openConnection(uri);
    connection.setUseCaches(true);
    if (localCacheOnly) {
      connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
    }

    int responseCode = connection.getResponseCode();
    if (responseCode >= 300) {
      connection.disconnect();
      throw new ResponseException(responseCode + " " + connection.getResponseMessage());
    }

    String responseSource = connection.getHeaderField(RESPONSE_SOURCE_OKHTTP);
    if (responseSource == null) {
      responseSource = connection.getHeaderField(RESPONSE_SOURCE_ANDROID);
    }

    long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
    boolean fromCache = parseResponseSourceHeader(responseSource);

    return new Response(connection.getInputStream(), fromCache, contentLength);
  }

过程类似。

至此,一个完整的获取图片的流程就可以看到了。

1.每一条图片请求将会包装成一个request。

2.每一个request将会submit到一个DispatcherThread的线程做分发操作。

3.找到request对应的RequestHandler。把bitmaphunter放入PicassoExecutorService线程池里面。

4.由不同的RequestHandler来处理各种情况,包括网络request。

5.剩下的就是结果的处理,和delivier response的过程了。

 

由于本文只关心网络图片的加载过程,所以只涉及了Picasso的2级缓存功能。

参考:

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/0731/1639.html

http://square.github.io/picasso/

 

本系列第一篇:Volley源码分析(1)----Volley 队列

 

 

posted on 2015-12-21 15:04  Joyfulmath  阅读(710)  评论(0编辑  收藏  举报

导航