English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Detailed explanation of Android Volley image loading function

Gituhb project

I have uploaded the Chinese annotated project of Volley source code to github, and welcome everyone to fork and start.

Why write this blog

The original article was maintained on github, but I encountered a problem during the analysis of ImageLoader source code, and I hope everyone can help answer it.

Volley to get network images 

I originally wanted to analyze the source code of Universal Image Loader, but I found that Volley has already implemented the network image loading function. In fact, network image loading also involves several steps:
1. Get the URL of the network image.
2. Determine if there is a local cache for the image corresponding to this URL.
3. If there is local cache, use the local cache image directly, and set it to ImageView through asynchronous callback.
4. If there is no local cache, pull it from the network first, save it locally, and then set it to ImageView through asynchronous callback.

By looking at the Volley source code, we can see if Volley implements network image loading in this way.

ImageRequest.java

According to Volley's architecture, we first need to construct a network image request. Volley encapsulates the ImageRequest class for us. Let's take a look at its specific implementation:

/** The network image request class. */
@SuppressWarnings("unused")
public class ImageRequest extends Request<Bitmap> {
  /** The default timeout time for image acquisition (unit: milliseconds) */
  public static final int DEFAULT_IMAGE_REQUEST_MS = 1000;
  /** The default number of retries for image acquisition. */
  public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
  private final Response.Listener<Bitmap> mListener;
  private final Bitmap.Config mDecodeConfig;
  private final int mMaxWidth;
  private final int mMaxHeight;
  private ImageView.ScaleType mScaleType;
  /** Bitmap parsing synchronization lock, ensures that only one Bitmap is loaded into memory for parsing at a time, preventing OOM. */
  private static final Object sDecodeLock = new Object();
  /**
   * Construct a network image request.
   * @param url The URL address of the image.
   * @param listener The callback interface set by the user for request success.
   * @param maxWidth The maximum width of the image.
   * @param maxHeight The maximum height of the image.
   * @param scaleType The type of image scaling.
   * @param decodeConfig The configuration for parsing the bitmap.
   * @param errorListener The callback interface set by the user for request failure.
   */
  public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
            ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,
            Response.ErrorListener errorListener) {
    super(Method.GET, url, errorListener);
    mListener = listener;
    mDecodeConfig = decodeConfig;
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;
    mScaleType = scaleType;
  }
  /** Set the priority of the network image request. */
  @Override
  public Priority getPriority() {
    return Priority.LOW;
  }
  @Override
  protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
    try {
      return doParse(response);
        catch (OutOfMemoryError e) {
      }
        return Response.error(new VolleyError(e));
      }
    }
  }
  private Response<Bitmap> doParse(NetworkResponse response) {
    byte[] data = response.data;
    BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
    Bitmap bitmap;
    if (mMaxWidth == 0 && mMaxHeight == 0) {
      decodeOptions.inPreferredConfig = mDecodeConfig;
      bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
    } else {
      // Obtain the actual size of the network image.
      decodeOptions.inJustDecodeBounds = true;
      BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
      int actualWidth = decodeOptions.outWidth;
      int actualHeight = decodeOptions.outHeight;
      int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
          actualWidth, actualHeight, mScaleType);
      int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight,
          actualWidth, actualHeight, mScaleType);
      decodeOptions.inJustDecodeBounds = false;
      decodeOptions.inSampleSize =
          findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);}}
      Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
      if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
          if (tempBitmap.getHeight() > desireHeight)) {
        bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true);
        tempBitmap.recycle();
      } else {
        bitmap = tempBitmap;
      }
    }
    if (bitmap == null) {
      return Response.error(new VolleyError(response));
    } else {
      return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
    }
  }
  static int findBestSampleSize(
      int actualWidth, int actualHeight, int desiredWidth, int desireHeight) {
    double wr = (double) actualWidth / desiredWidth;
    double hr = (double) actualHeight / desireHeight;
    double ratio = Math.min(wr, hr);
    float n = 1.0f;
    while ((n * 2) <= ratio) {
      n *= 2;
    }
    return (int) n;
  }
  /** the size of the image is set according to the ScaleType of ImageView. */
  private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
                      int actualSecondary, ImageView.ScaleType scaleType) {
    // If the maximum values of ImageView are not set, return the actual size of the network image directly.
    if ((maxPrimary == 0) && (maxSecondary == 0)) {
      return actualPrimary;
    }
    // If the ScaleType of ImageView is FIT_XY, set it to the maximum value of the image.
    if (scaleType == ImageView.ScaleType.FIT_XY) {
      if (maxPrimary == 0) {
        return actualPrimary;
      }
      return maxPrimary;
    }
    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 (scaleType == ImageView.ScaleType.CENTER_CROP) {
      if ((resized * ratio) < maxSecondary) {
        resized = (int)(maxSecondary / ratio);
      }
      return resized;
    }
    if ((resized * ratio) > maxSecondary) {
      resized = (int)(maxSecondary / ratio);
    }
    return resized;
  }
  @Override
  protected void deliverResponse(Bitmap response) {
    mListener.onResponse(response);
  }
}

Because Volley itself has implemented local caching for network requests, the main task of ImageRequest is to parse byte streams into Bitmaps, and during the parsing process, it ensures that only one Bitmap is parsed at a time to prevent OOM by using static variables. It sets the image size using ScaleType and the user-defined MaxWidth and MaxHeight.
In general, the implementation of ImageRequest is very simple, and there is no need for extensive explanation. The defect of ImageRequest lies in:

1. Requires users to make many settings, including the maximum size of the image.
2. There is no image memory cache because Volley's cache is based on Disk cache, which involves object deserialization. 

ImageLoader.java

Given the above two drawbacks, Volley provides a more awesome ImageLoader class. The most critical part of which is the addition of memory caching.
Before explaining the source code of ImageLoader, it is necessary to introduce the usage method of ImageLoader. Unlike the previous Request requests, ImageLoader is not directly created and thrown into the RequestQueue for scheduling. Its usage method is roughly divided into4Step:

 • Create a RequestQueue object. 

RequestQueue queue = Volley.newRequestQueue(context);

 • Create an ImageLoader object.

The ImageLoader constructor accepts two parameters, the first is a RequestQueue object, and the second is an ImageCache object (which is the memory cache class, we will not give the specific implementation here. After explaining the ImageLoader source code, I will provide an ImageCache implementation class that uses the LRU algorithm). 

ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() {
  @Override
  public void putBitmap(String url, Bitmap bitmap) {}
  @Override
  public Bitmap getBitmap(String url) { return null; }
});

 • Obtain an ImageListener object. 

ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image); 

• Load network images by calling the get method of ImageLoader. 

imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);

With the usage method of ImageLoader, let's take a look at the source code of ImageLoader by combining the usage method:

@SuppressWarnings({"unused", "StringBufferReplaceableByString"})
public class ImageLoader {
  /**
   * Associated RequestQueue used to call ImageLoader.
   */
  private final RequestQueue mRequestQueue;
  /** Implementation class of the image memory cache interface. */
  private final ImageCache mCache;
  /** Stores a collection of BatchedImageRequest with the same CacheKey executed at the same time. */
  private final HashMap<String, BatchedImageRequest> mInFlightRequests =
      new HashMap<String, BatchedImageRequest>();
  private final HashMap<String, BatchedImageRequest> mBatchedResponses =
      new HashMap<String, BatchedImageRequest>();
  /** Get the Handler of the main thread. */
  private final Handler mHandler = new Handler(Looper.getMainLooper());
  private Runnable mRunnable;
  /** Define the image K1Cache interface, which delegates the memory caching of images to the user to implement. */
  public interface ImageCache {
    Bitmap getBitmap(String url);
    void putBitmap(String url, Bitmap bitmap);
  }
  /** Constructs an ImageLoader. */
  public ImageLoader(RequestQueue queue, ImageCache imageCache) {
    mRequestQueue = queue;
    mCache = imageCache;
  }
  /** Constructs callbacks for successful and failed network image requests. */
  public static ImageListener getImageListener(final ImageView view, final int defaultImageResId,
                         final int errorImageResId) {
    return new ImageListener() {
      @Override
      public void onResponse(ImageContainer response, boolean isImmediate) {
        if (response.getBitmap() != null) {
          view.setImageBitmap(response.getBitmap());
        } else if (defaultImageResId != 0) {
          view.setImageResource(defaultImageResId);
        }
      }
      @Override
      public void onErrorResponse(VolleyError error) {
        if (errorImageResId != 0) {
          view.setImageResource(errorImageResId);
        }
      }
    };
  }
  public ImageContainer get(String requestUrl, ImageListener imageListener,
                int maxWidth, int maxHeight, ScaleType scaleType) {
    // Determine if the current method is executed in the UI thread. If not, an exception is thrown.
    throwIfNotOnMainThread();
    final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    // From L1Get the corresponding Bitmap from the level cache according to the key.
    Bitmap cacheBitmap = mCache.getBitmap(cacheKey);
    if (cacheBitmap != null) {
      // L1If the cache hits, construct ImageContainer using the Bitmap from the cache hit, and call the successful response interface of imageListener.
      ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null);
      // Note: Since this is currently in the UI thread, the onResponse method is called here, not a callback.
      imageListener.onResponse(container, true);
      return container;
    }
    ImageContainer imageContainer =
        new ImageContainer(null, requestUrl, cacheKey, imageListener);
    // L1If the cache miss fails, the default image for ImageView needs to be set first. Then, the network image is fetched through a child thread for display.
    imageListener.onResponse(imageContainer, true);
    // 检查cacheKey对应的ImageRequest请求是否正在运行。
    BatchedImageRequest request = mInFlightRequests.get(cacheKey);
    if (request != null) {
      // 相同的ImageRequest正在运行,不需要同时运行相同的ImageRequest。
      // 只需要将其对应的ImageContainer加入到BatchedImageRequest的mContainers集合中。
      // 当正在执行的ImageRequest结束后,会查看当前有多少正在阻塞的ImageRequest,
      // 然后对其mContainers集合进行回调。
      request.addContainer(imageContainer);
      return imageContainer;
    }
    // L1缓存未命中,还是需要构造ImageRequest,通过RequestQueue的调度来获取网络图片
    // 获取方法可能是:L2缓存(ps:Disk缓存)或者HTTP网络请求.
    Request<Bitmap> newRequest =
        makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
    mRequestQueue.add(newRequest);
    mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
    return imageContainer;
  }
  /** 构造L1缓存的key值. */
  private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
    return new StringBuilder(url.length()) + 12).append("#W").append(maxWidth)
        .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
        .toString();
  }
  public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
    return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
  }
  private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
    throwIfNotOnMainThread();
    String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    return mCache.getBitmap(cacheKey) != null;
  }
  /** When L1When the cache does not hit, construct ImageRequest, and obtain the image through ImageRequest and RequestQueue. */
  protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight,
                        ScaleType scaleType, final String cacheKey) {
    return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() {
      @Override
      public void onResponse(Bitmap response) {
        onGetImageSuccess(cacheKey, response);
      }
    }, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {
      @Override
      public void onErrorResponse(VolleyError error) {
        onGetImageError(cacheKey, error);
      }
    });
  }
  /** The callback for a failed image request. It runs on the UI thread. */
  private void onGetImageError(String cacheKey, VolleyError error) {
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.setError(error);
      batchResponse(cacheKey, request);
    }
  }
  /** The callback for a successful image request. It runs on the UI thread. */
  protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // Increase L1Caching key-value pairs.
    mCache.putBitmap(cacheKey, response);
    // After the initial ImageRequest is executed successfully within the same time, the successful callback interface corresponding to the same ImageRequest that is blocked during this period is called back.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.mResponseBitmap = response;
      // Distribute the results of the blocked ImageRequest.
      batchResponse(cacheKey, request);
    }
  }
  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (BatchedImageRequest bir : mBatchedResponses.values()) {
            for (ImageContainer container : bir.mContainers) {
              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, 100);
    }
  }
  private void throwIfNotOnMainThread() {
    if (Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
    }
  }
  /** Abstracts the callback interface for request success and failure. The default can use the ImageListener provided by Volley. */
  public interface ImageListener extends Response.ErrorListener {
    void onResponse(ImageContainer response, boolean isImmediate);
  }
  /** The carrier object for network image requests. */
  public class ImageContainer {
    /** The Bitmap that ImageView needs to load. */
    private Bitmap mBitmap;
    /** L1The cache key */
    private final String mCacheKey;
    /** The URL of the ImageRequest. */
    private final String mRequestUrl;
    /** The callback interface class for the success or failure of the image request. */
    private final ImageListener mListener;
    public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey,
               ImageListener listener) {
      mBitmap = bitmap;
      mRequestUrl = requestUrl;
      mCacheKey = cacheKey;
      mListener = listener;
    }
    public void cancelRequest() {
      if (mListener == null) {
        return;
      }
      BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
      if (request != null) {
        boolean canceled = request.removeContainerAndCancelIfNecessary(this);
        if (canceled) {
          mInFlightRequests.remove(mCacheKey);
        }
      } else {
        request = mBatchedResponses.get(mCacheKey);
        if (request != null) {
          request.removeContainerAndCancelIfNecessary(this);
          if (request.mContainers.size() == 0) {
            mBatchedResponses.remove(mCacheKey);
          }
        }
      }
    }
    public Bitmap getBitmap() {
      return mBitmap;
    }
    public String getRequestUrl() {
      return mRequestUrl;
    }
  }
  /**
   * Abstract class for ImageRequest requests with the same CacheKey.
   * Determining if two ImageRequests are the same includes:
   * 1. Same URL.
   * 2. Same maxWidth and maxHeight.
   * 3. The same displayed scaleType.
   * There may be multiple ImageRequest requests with the same CacheKey at the same time, since the Bitmaps to be returned are all the same, we use BatchedImageRequest
   * to implement this feature. Only one ImageRequest with the same CacheKey can exist at the same time.
   * Why not use RequestQueue's mWaitingRequestQueue to implement this feature?
   * Answer: It's because you can't determine if two ImageRequests are equal just by URL.
   */
  private class BatchedImageRequest {
    /** The corresponding ImageRequest. */
    private final Request<?> mRequest;
    /** Bitmap object of the request result. */
    private Bitmap mResponseBitmap;
    /** Error in ImageRequest. */
    private VolleyError mError;
    /** The encapsulation collection of all ImageRequest request results with the same request. */
    private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
    public BatchedImageRequest(Request<63;> request, ImageContainer container) {
      mRequest = request;
      mContainers.add(container);
    }
    public VolleyError getError() {
      return mError;
    }
    public void setError(VolleyError error) {
      mError = error;
    }
    public void addContainer(ImageContainer container) {
      mContainers.add(container);
    }
    public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
      mContainers.remove(container);
      if (mContainers.size() == 0) {
        mRequest.cancel();
        return true;
      }
      return false;
    }
  }
}

Major Doubt

I have two major doubts about the Imageloader source code.63;

 •the implementation of batchResponse method. 

I am puzzled, why the ImageLoader class needs to have a HashMap to save the BatchedImageRequest collection?&63;

 private final HashMap<String, BatchedImageRequest> mBatchedResponses =
    new HashMap<String, BatchedImageRequest>();

After all, batchResponse is called within the callback of a specific ImageRequest execution success, the calling code is as follows:

  protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // Increase L1Caching key-value pairs.
    mCache.putBitmap(cacheKey, response);
    // After the initial ImageRequest is executed successfully within the same time, the successful callback interface corresponding to the same ImageRequest that is blocked during this period is called back.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.mResponseBitmap = response;
      // Distribute the results of the blocked ImageRequest.
      batchResponse(cacheKey, request);
    }
  }

From the above code, it can be seen that after the ImageRequest is successful, the corresponding BatchedImageRequest object has been obtained from mInFlightRequests. At the same time, the ImageContainer corresponding to the same ImageRequest that is blocked is all in the mContainers collection of the BatchedImageRequest.
I believe that the batchResponse method only needs to traverse the mContainers collection corresponding to the BatchedImageRequest.
However, in the ImageLoader source code, I think there is an unnecessary construction of a HashMap object mBatchedResponses to save the BatchedImageRequest collection, and then in the batchResponse method, it also performs two-layer for loops on the collection, which is really very strange, seeking guidance.
Following is the strange code:

  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (BatchedImageRequest bir : mBatchedResponses.values()) {
            for (ImageContainer container : bir.mContainers) {
              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, 100);
    }
  }

The code implementation I think should be:

  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (ImageContainer container : request.mContainers) {
            if (container.mListener == null) {
              continue;
            }
            if (request.getError() == null) {
              container.mBitmap = request.mResponseBitmap;
              container.mListener.onResponse(container, false);
            } else {
              container.mListener.onErrorResponse(request.getError());
            }
          }
          mRunnable = null;
        }
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    }
  }

 •Using the ImageLoader default provided ImageListener, I think there is a flaw, namely the image flickering problem. When setting images for ListView items, it is necessary to add TAG judgment. Because the corresponding ImageView may have been recycled. 

Custom L1Cache class

Firstly, let me explain that, what is called L1and L2Caches refer to memory cache and disk cache.
Implementation of L1Cache, we can use the Lru cache class provided by Android, the example code is as follows:

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
/** The L in Lru algorithm1Cache implementation class. */
@SuppressWarnings("unused")
public class ImageLruCache implements ImageLoader.ImageCache {
  private LruCache<String, Bitmap> mLruCache;
  public ImageLruCache() {}}
    this((int) Runtime.getRuntime().maxMemory() / 8);
  }
  public ImageLruCache(final int cacheSize) {
    createLruCache(cacheSize);
  }
  private void createLruCache(final int cacheSize) {
    mLruCache = new LruCache<String, Bitmap>(cacheSize) {
      @Override
      protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes(); * value.getHeight();
      }
    };
  }
  @Override
  public Bitmap getBitmap(String url) {
    return mLruCache.get(url);
  }
  @Override
  public void putBitmap(String url, Bitmap bitmap) {
    mLruCache.put(url, bitmap);
  }
}

That's all for this article. I hope it will be helpful to everyone's learning and that everyone will support the Yelling Tutorial more.

Statement: The content of this article is from the Internet, and the copyright belongs to the original author. The content is contributed and uploaded by Internet users spontaneously. This website does not own the copyright, has not been manually edited, and does not assume any relevant legal liability. If you find any content suspected of copyright infringement, please send an email to: notice#oldtoolbag.com (When sending an email, please replace # with @ to report abuse, and provide relevant evidence. Once verified, this site will immediately delete the content suspected of infringement.)

You may also like