zoukankan      html  css  js  c++  java
  • 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 队列

     

  • 相关阅读:
    BIP_开发案例08_BI Publisher图表示例 饼状图/直方图/折线图(案例)
    BIP_开发案例07_将原有Report Builer报表全部转为XML Publisher形式(案例)
    Form_Form Builder Export导出为Excel(案例)
    BIP_开发案例06_以RB.RDF为数据源BIP.RTF为模板的简单例子(案例)
    Form_Form Builder开发基于视图页面和自动代码生成包(案例)
    BIP_开发案例05_BI Pubisher标准做法以BIP.XML为数据源以BIP.RTF为模板的简单例子(案例)
    BIP_开发案例04_通过BI Publisher实现打印报表的二维码(案例)(待整理)
    BIP_开发案例03_将原有Report Builer报表全部转为XML Publisher形式(案例)
    BIP_开发案例02_BI Publisher中复杂案例实现代码(案例)
    BIP_开发案例01_BI Publisher报表手工提交和控制(案例)
  • 原文地址:https://www.cnblogs.com/deman/p/5062830.html
Copyright © 2011-2022 走看看