Github Project
I have uploaded the Volley source code Chinese annotation project to github. Everyone is welcome to fork and start.
Why write this blog
Originally, the article was maintained on github, but I encountered a question during the analysis of ImageLoader source code. I hope everyone can help answer it.
Volley gets online pictures
I originally wanted to analyze the source code of Universal Image Loader, but I found that Volley has implemented the loading function of network images. In fact, the loading of network images is also divided into several steps:
1. Get the url of the network image.
2. Determine whether the image corresponding to the url has a local cache.
3. With local cache, use local cache images directly and set up ImageView through asynchronous callbacks.
4. Without local cache, you can first pull it from the network, save it locally, and then set it to the ImageView through asynchronous callbacks.
Let’s use the Volley source code to see if Volley follows this step to realize network image loading.
According to Volley's architecture, we first need to construct a network image request. Volley helped us encapsulate the ImageRequest class. Let's take a look at its specific implementation:
/** Network image request class. */ @SuppressWarnings("unused") public class ImageRequest extends Request<Bitmap> { /** Default image acquisition timeout time (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 <Bitmap> mListener; private final mDecodeConfig; private final int mMaxWidth; private final int mMaxHeight; private mScaleType; /** Bitmap parses synchronization locks, ensuring that only one Bitmap is loaded into memory for parsing at the same time, preventing OOM. */ private static final Object sDecodeLock = new Object(); /** * Construct a network image request. * @param url The URL address of the picture. * @param listener Requests the callback interface set by the successful user. * @param maxWidth Maximum width of the image. * @param maxHeight Maximum height of the picture. * @param scaleType Image Scaling Type. * @param decodeConfig parses the configuration of bitmap. * @param errorListener The callback interface set by the user failed request. */ public ImageRequest(String url, <Bitmap> listener, int maxWidth, int maxHeight, scaleType, decodeConfig, errorListener) { super(, url, errorListener); mListener = listener; mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; mScaleType = scaleType; } /** Set the priority of network image requests. */ @Override public Priority getPriority() { return ; } @Override protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { synchronized (sDecodeLock) { try { return doParse(response); } catch (OutOfMemoryError e) { return (new VolleyError(e)); } } } private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = ; decodeOptions = new (); Bitmap bitmap; if (mMaxWidth == 0 && mMaxHeight == 0) { = mDecodeConfig; bitmap = (data, 0, , decodeOptions); } else { // Get the real size of the network image. = true; (data, 0, , decodeOptions); int actualWidth = ; int actualHeight = ; int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight, mScaleType); = false; = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight); Bitmap tempBitmap = (data, 0, , decodeOptions); if (tempBitmap != null && (() > desiredWidth || () > desireHeight)) { bitmap = (tempBitmap, desiredWidth, desireHeight, true); (); } else { bitmap = tempBitmap; } } if (bitmap == null) { return (new VolleyError(response)); } else { return (bitmap, (response)); } } static int findBestSampleSize( int actualWidth, int actualHeight, int desiredWidth, int desireHeight) { double wr = (double) actualWidth / desiredWidth; double hr = (double) actualHeight / desireHeight; double ratio = (wr, hr); float n = 1.0f; while ((n * 2) <= ratio) { n *= 2; } return (int) n; } /** Set the size of the image according to the ScaleType of the ImageView. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, scaleType) { // If the maximum value of ImageView is not set, the real size of the network image will be directly returned. if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } // If the ScaleType of ImageView is FIX_XY, set it to the image's most value. if (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 == .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) { (response); } }
Because Volley's framework has implemented local cache of network requests, the main thing that ImageRequest does is to parse the byte stream to Bitmap. During the parsing process, static variables are used to ensure that only one Bitmap is parsed at a time to prevent OOM, and use ScaleType and the MaxWidth and MaxHeight set by the user to set the image size.
Overall, the implementation of ImageRequest is very simple, so I won't explain it too much here. The disadvantage of ImageRequest is:
1. The user needs to make too many settings, including the maximum size of the image.
2. There is no memory cache for images, because Volley's cache is based on Disk cache, and there is a process of object deserialization.
In view of the above two shortcomings, Volley provides a more awesome ImageLoader class. Among them, the most important thing is to add memory cache.
Before explaining the source code of ImageLoader, we need to introduce the usage method of ImageLoader. Unlike the previous Request request, ImageLoader is not thrown out new and directly to the RequestQueue for scheduling. Its usage method is roughly divided into 4 steps:
•Create a RequestQueue object.
RequestQueue queue = (context);
•Create an ImageLoader object.
The ImageLoader constructor receives two parameters. The first is the RequestQueue object and the second is the ImageCache object (that is, the memory cache class. We will not give the specific implementation first. 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; } });
• Get an ImageListener object.
ImageListener listener = (imageView, .default_imgage, .failed_image);
•Calling the get method of ImageLoader to load network images.
(mImageUrl, listener, maxWidth, maxHeight, scaleType);
With the usage method of ImageLoader, let’s take a look at the source code of ImageLoader based on the usage method:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * Associate the RequestQueue used to call ImageLoader. */ private final RequestQueue mRequestQueue; /** Image memory cache interface implementation class. */ private final ImageCache mCache; /** Stores the BatchedImageRequest collection of 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(()); private Runnable mRunnable; /** Define the image K1 cache interface, and hand over the memory cache of the image to the user to implement it. */ public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); } /** Construct an ImageLoader. */ public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; } /** Constructs a callback interface 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 (() != null) { (()); } else if (defaultImageResId != 0) { (defaultImageResId); } } @Override public void onErrorResponse(VolleyError error) { if (errorImageResId != 0) { (errorImageResId); } } }; } public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { // Determine whether the current method is executed in the UI thread. If not, an exception is thrown. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // Get the corresponding Bitmap from the L1 level cache according to the key. Bitmap cacheBitmap = (cacheKey); if (cacheBitmap != null) { // L1 caches hits, constructs the ImageContainer through the cache hit Bitmap, and calls the imageListener's response interface successfully. ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null); // Note: Because it is currently in the UI thread, the onResponse method is called here, not a callback. (container, true); return container; } ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // If the L1 cache hit fails, you need to set the default image for ImageView first. Then pull the network image through the child thread and display it. (imageContainer, true); // Check whether the ImageRequest request corresponding to cacheKey is running. BatchedImageRequest request = (cacheKey); if (request != null) { // The same ImageRequest is running, and there is no need to run the same ImageRequest at the same time. // Just add its corresponding ImageContainer to the mContainers collection of BatchedImageRequest. // When the execution ImageRequest ends, it will check how many blocking ImageRequests are currently checked. // Then make a callback to its mContainers collection. (imageContainer); return imageContainer; } // The L1 cache is not hit, but the ImageRequest is still necessary to construct and obtain network images through the scheduling of the RequestQueue. // The method of obtaining may be: L2 cache (ps:Disk cache) or HTTP network request. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); (newRequest); (cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; } /** Constructs the key value of L1 cache. */ private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) { return new StringBuilder(() + 12).append("#W").append(maxWidth) .append("#H").append(maxHeight).append("#S").append(()).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 (cacheKey) != null; } /** When the L1 cache does not hit, construct ImageRequest and get 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 <Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, scaleType, .RGB_565, new () { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); } /** The image request failed callback. Runs in the UI thread. */ private void onGetImageError(String cacheKey, VolleyError error) { BatchedImageRequest request = (cacheKey); if (request != null) { (error); batchResponse(cacheKey, request); } } /** The image request successfully called back. Runs in the UI thread. */ protected void onGetImageSuccess(String cacheKey, Bitmap response) { // Add key-value pairs to L1 cache. (cacheKey, response); // After the initial ImageRequest execution is successful at the same time, the callback interface corresponding to the same ImageRequest blocked during this period is called back. BatchedImageRequest request = (cacheKey); if (request != null) { = response; // Distribute the blocking ImageRequest result. batchResponse(cacheKey, request); } } private void batchResponse(String cacheKey, BatchedImageRequest request) { (cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : ()) { for (ImageContainer container : ) { if ( == null) { continue; } if (() == null) { = ; (container, false); } else { (()); } } } (); mRunnable = null; } }; // Post the runnable (mRunnable, 100); } } private void throwIfNotOnMainThread() { if (() != ()) { throw new IllegalStateException("ImageLoader must be invoked from the main thread."); } } /** Abstract the callback interface for successful and failed requests. By default, the ImageListener provided by Volley can be used. */ public interface ImageListener extends { void onResponse(ImageContainer response, boolean isImmediate); } /** The bearer object for network image request. */ public class ImageContainer { /** Bitmap that ImageView needs to load. */ private Bitmap mBitmap; /** L1 cached key */ private final String mCacheKey; /** url requested by ImageRequest. */ private final String mRequestUrl; /** Callback interface class where image request succeeds or fails. */ 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 = (mCacheKey); if (request != null) { boolean canceled = (this); if (canceled) { (mCacheKey); } } else { request = (mCacheKey); if (request != null) { (this); if (() == 0) { (mCacheKey); } } } } public Bitmap getBitmap() { return mBitmap; } public String getRequestUrl() { return mRequestUrl; } } /** * CacheKey same ImageRequest request abstract class. * Determining that the two ImageRequests are the same include: * 1. The same url. * 2. maxWidth and maxHeight are the same. * 3. The scaleType displayed is the same. * There may be multiple ImageRequest requests for the same CacheKey at the same time. Since the Bitmap that needs to be returned are the same, so use BatchedImageRequest * to implement this function. There can only be one ImageRequest with the same CacheKey at the same time. * Why not use the mWaitingRequestQueue's mWaitingRequestQueue to implement this function? * Answer: It is because it is impossible to judge that the two ImageRequests are equal by relying solely on the URL. */ private class BatchedImageRequest { /** Corresponding ImageRequest request. */ private final Request<?> mRequest; /** Bitmap object for request result. */ private Bitmap mResponseBitmap; /** Error in ImageRequest. */ private VolleyError mError; /** Encapsulated collection of all the same ImageRequest request results. */ private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>(); public BatchedImageRequest(Request<?> request, ImageContainer container) { mRequest = request; (container); } public VolleyError getError() { return mError; } public void setError(VolleyError error) { mError = error; } public void addContainer(ImageContainer container) { (container); } public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { (container); if (() == 0) { (); return true; } return false; } } }
Major questions
I have two major questions about the source code of Imageloader?
• Implementation of the batchResponse method.
I'm very surprised, why does the ImageLoader class have a HashMap to save the BatchedImageRequest collection?
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
After all, batchResponse is called in a callback that is successfully executed by a specific ImageRequest, and the calling code is as follows:
protected void onGetImageSuccess(String cacheKey, Bitmap response) { // Add key-value pairs to L1 cache. (cacheKey, response); // After the initial ImageRequest execution is successful at the same time, the callback interface corresponding to the same ImageRequest blocked during this period is called back. BatchedImageRequest request = (cacheKey); if (request != null) { = response; // Distribute the blocking ImageRequest result. batchResponse(cacheKey, request); } }
From the above code, we can see that after the ImageRequest request is successful, the corresponding BatchedImageRequest object has been obtained from mInFlightRequests. The ImageContainers corresponding to the same ImageRequest that are blocked at the same time are all in the mContainers collection of BatchedImageRequest.
Then I think that the batchResponse method only needs to traverse the mContainers collection corresponding to the BatchedImageRequest.
However, in the ImageLoader source code, I think it is unnecessary to construct a HashMap object mBatchedResponses to save the BatchedImageRequest collection, and then perform two-layer for loops on the collection in the batchResponse method. It is really weird, so please guide.
The weird code is as follows:
private void batchResponse(String cacheKey, BatchedImageRequest request) { (cacheKey, request); if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : ()) { for (ImageContainer container : ) { if ( == null) { continue; } if (() == null) { = ; (container, false); } else { (()); } } } (); mRunnable = null; } }; // Post the runnable (mRunnable, 100); } }
My opinion code implementation should be:
private void batchResponse(String cacheKey, BatchedImageRequest request) { if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (ImageContainer container : ) { if ( == null) { continue; } if (() == null) { = ; (container, false); } else { (()); } } mRunnable = null; } }; // Post the runnable (mRunnable, 100); } }
• Using the ImageListener provided by ImageLoader by default, I think there is a flaw, that is, the image flash problem. When setting the image for the ListView item, you need to add TAG judgment. Because the corresponding ImageView may have been recycled.
Custom L1 cache class
First of all, the so-called L1 and L2 caches refer to memory cache and hard disk cache respectively.
To implement L1 cache, we can use the Lru cache class provided by Android. The sample code is as follows:
import ; import .; /** L1 cache implementation class of Lru algorithm. */ @SuppressWarnings("unused") public class ImageLruCache implements { private LruCache<String, Bitmap> mLruCache; public ImageLruCache() { this((int) ().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 () * (); } }; } @Override public Bitmap getBitmap(String url) { return (url); } @Override public void putBitmap(String url, Bitmap bitmap) { (url, bitmap); } }
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.