English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Проект на GitHub
Проект китайских комментариев к исходному коду Volley уже загружен на github, все желающие могут fork и start.
Почему я пишу эту статью
Статья была mantenana на github, но во время анализа исходного кода ImageLoader я встретил проблему, и希望大家 помогут ее решить.
Volley загружает сетевое изображение
Я хотел проанализировать исходный код Universal Image Loader, но обнаружил, что Volley уже реализует функцию загрузки сетевых изображений. На самом деле, загрузка сетевых изображений также разделена на несколько шагов:
1. Получается URL сетевого изображения.
2. Проверяется, есть ли локальный кэш для изображения, соответствующего этому URL.
3. Если есть локальный кэш, используется изображение из локального кэша, и через асинхронный вызов устанавливается в ImageView.
4. Если нет локального кэша, сначала загружается изображение из сети, сохраняется локально, а затем через асинхронный вызов устанавливается в ImageView.
Изучая исходный код Volley, посмотрим, реализует ли Volley загрузку сетевых изображений по этому шагу.
ImageRequest.java
Согласно архитектуре Volley, нам сначала нужно создать запрос к сетевому изображению. Volley предоставляет нам класс ImageRequest, давайте посмотрим на его реализацию:
/** Класс запроса к сетевому изображению. */ @SuppressWarnings("unused") public class ImageRequest extends Request<Bitmap> { /** Время ожидания получения изображения по умолчанию (в миллисекундах) */ public static final int DEFAULT_IMAGE_REQUEST_MS = 1000; /** Количество попыток повторного получения изображения по умолчанию. */ 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, гарантирует, что в одно и то же время в память загружается только один Bitmap, что предотвращает OOM. */ private static final Object sDecodeLock = new Object(); /** * Создание запроса к сетевому изображению. * @param url Адрес изображения. * @param decodeConfig Конфигурация анализа bitmap. * @param errorListener Интерфейс обратного вызова для запроса,失败的回调接口. 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); */ mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; mScaleType = scaleType; mListener = listener; /** Установить приоритет запроса сетевой картинки. */ } public Priority getPriority() { return Priority.LOW; {} protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { @Override synchronized (sDecodeLock) { } {} @Override 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 { // Получение реальных размеров изображения из сети. 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 || 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; {} /** Установите размер изображения в зависимости от ScaleType ImageView. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ImageView.ScaleType scaleType) { // Если не было установлено максимальное значение ImageView, верните реальный размер сети изображения. if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; {} // Если ScaleType ImageView FIX_XY, то установите его значение на наибольшее значение изображения. 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); {} {}
Поскольку сам фреймворк Volley уже реализует кэширование сетевых запросов на локальном уровне, основная задача ImageRequest заключается в преобразовании потоков байт в Bitmap и в процессе преобразования, через статические переменные гарантируется, что каждый раз преобразуется только один Bitmap, чтобы предотвратить ООМ (Out of Memory), использование ScaleType и установленных пользователем MaxWidth и MaxHeight для установки размера изображения.
В общем, реализация ImageRequest очень проста, поэтому здесь не будет做过多的 объяснений. Недостатком ImageRequest является:
1. Необходимость выполнения большого количества настроек пользователем, включая максимальный размер изображения.
2. Отсутствие кэширования изображений в памяти, так как кэширование Volley основано на кэше на диске, что включает процесс десериализации объектов.
ImageLoader.java
Учитывая вышеупомянутые два недостатка, Volley предоставляет более продвинутый класс ImageLoader, в котором наиболее важным является добавление кэширования в памяти.
Прежде чем перейти к изучению исходного кода ImageLoader, необходимо сначала рассказать о методах использования ImageLoader. В отличие от предыдущих запросов Request, ImageLoader не создается напрямую и передается в RequestQueue для调度, его использование можно разделить на 4 основных шага:
• Создание объекта RequestQueue.
RequestQueue queue = Volley.newRequestQueue(context);
• Создание объекта ImageLoader.
Конструктор ImageLoader принимает два параметра: объект RequestQueue и объект ImageCache (это класс кэширования в памяти, мы не будем предоставлять его конкретное реализация, после того как мы завершим обсуждение исходного кода ImageLoader, я предоставлю реализацию ImageCache с использованием алгоритма LRU)
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() { @Override public void putBitmap(String url, Bitmap bitmap) {} @Override public Bitmap getBitmap(String url) { return null; } });
• Получение объекта ImageListener.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
• Вызов метода get ImageLoader для загрузки изображения из сети.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
Используя ImageLoader, давайте рассмотрим его исходный код, чтобы понять, как он используется:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * Связан с RequestQueue для вызова ImageLoader. */ private final RequestQueue mRequestQueue; /** Реализация интерфейса ImageCache для кэширования изображений в памяти. */ private final ImageCache mCache; /** Содержит набор BatchedImageRequest с одинаковым CacheKey, выполняемые в одно и то же время. */ private final HashMap<String, BatchedImageRequest> mInFlightRequests =; new HashMap<String, BatchedImageRequest>(); private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>(); /** Получает Handler основного потока. */ private final Handler mHandler = new Handler(Looper.getMainLooper()); private Runnable mRunnable; /** Определяет интерфейс кэширования изображений K1,即将 кэширование изображений в памяти доверяет пользователю. */ public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); {} /** Создает ImageLoader. */ public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; {} /** Создает интерфейс вызова успешного и неуспешного вызова запроса сетевой картинки. */ 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 getImageContainer(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { int maxWidth, int maxHeight, ScaleType scaleType) { throwIfNotOnMainThread(); // Определите, выполняется ли текущий метод в UI-потоке. Если нет, выбросьте исключение. // Получите соответствующий Bitmap из L1 кэша по ключу. Bitmap cacheBitmap = mCache.getBitmap(cacheKey); if (cacheBitmap != null) { // Если кэш命中 L1, то через Bitmap, полученный из кэша, строим ImageContainer и вызываем интерфейс успешного ответа imageListener. ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null); // Внимание: так как сейчас выполняется на UI-потоке, поэтому вызывается метод onResponse, а не кэшбэк. imageListener.onResponse(container, true); return container; {} ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // Если не удалось命中 L1 кэш, сначала нужно установить изображение по умолчанию для ImageView. Затем через дочерний поток загрузить изображение из сети и показать его. imageListener.onResponse(imageContainer, true); // Проверьте, выполняется ли запрос ImageRequest, соответствующий cacheKey. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // Текущий ImageRequest уже выполняется, не нужно запускать его одновременно. // Добавьте соответствующий ImageContainer в набор mContainers BatchedImageRequest. // После завершения выполнения ImageRequest проверяется, сколько ImageRequest еще находятся в ожидании, // Затем выполняется вызов обратного вызова для集合а mContainers. request.addContainer(imageContainer); return imageContainer; {} // Кэш L1 не найден, поэтому необходимо создать ImageRequest, чтобы получить изображение через планировщик RequestQueue // Метод получения может быть: кэш L2 (примечание: кэш на диске) или сетевая HTTP-запрос. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; {} /** Создание ключа кэша L1. */ 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; {} /** При отсутствии命中 в кэше L1, создается ImageRequest, через ImageRequest и 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); {} }); {} /** Изображение не загружено успешно. Выполняется на UI-потоке. */ private void onGetImageError(String cacheKey, VolleyError error) { BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.setError(error); batchResponse(cacheKey, request); {} {} /** Изображение загружено успешно. Выполняется на UI-потоке. */ protected void onGetImageSuccess(String cacheKey, Bitmap response) { // Добавление пары ключ-значение в L1 кэш. mCache.putBitmap(cacheKey, response); // В同一 время после успешного выполнения первой ImageRequest, вызывается успешный обратный вызов, связанный с этой ImageRequest. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; // Раздача результатов заблокированных 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 должен быть вызван из основного потока."); {} {} /** Абстрактный интерфейс для вызова успешного и неудачного запроса. По умолчанию можно использовать ImageListener, предоставляемый Volley. */ public interface ImageListener extends Response.ErrorListener { void onResponse(ImageContainer response, boolean isImmediate); {} /** Объект载体 для запроса сетевого изображения. */ public class ImageContainer { /** Bitmap, который нужно загрузить ImageView. */ private Bitmap mBitmap; /** Ключ L1 кэша. */ private final String mCacheKey; /** URL запроса ImageRequest. */ private final String mRequestUrl; /** Класс интерфейса回调 для успешного или неудачного запроса изображения. */ 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; {} {} /** * Абстрактный класс запроса ImageRequest с одинаковым CacheKey. * Определение того, равны ли два ImageRequest, включает: * 1. Одинаковые url. * 2. Одинаковые maxWidth и maxHeight. * 3. Одинаковый scaleType. * В одно и то же время может быть несколько запросов ImageRequest с одинаковым CacheKey, так как все возвращаемые Bitmapы одинаковы, поэтому используется BatchedImageRequest * Для реализации этой функции. В одно и то же время может быть только один ImageRequest с одинаковым CacheKey. * Почему не использовать mWaitingRequestQueue из RequestQueue для реализации этой функции? * Ответ: это потому, что только по URL нельзя определить, равны ли два ImageRequest. */ private class BatchedImageRequest { /** Соответствующий запрос ImageRequest. */ private final Request<?> mRequest; /** Bitmap объекта результата запроса. */ private Bitmap mResponseBitmap; /** Ошибка ImageRequest. */ private VolleyError mError; /** Все обертки коллекций результатов ImageRequest, которые являются одинаковыми. */ private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>(); public BatchedImageRequest(Request<?> 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; {} {} {}
Основные вопросы
У меня есть две основные疑问 по исходному коду Imageloader?
• Реализация метода batchResponse.
Я очень удивлен, почему в классе ImageLoader используется HashMap для сохранения集合 BatchedImageRequest?
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
В конце концов, batchResponse вызывается в обратном вызове的成功ного выполнения специфического ImageRequest, код вызова выглядит следующим образом:
protected void onGetImageSuccess(String cacheKey, Bitmap response) { // Добавление пары ключ-значение в L1 кэш. mCache.putBitmap(cacheKey, response); // В同一 время после успешного выполнения первой ImageRequest, вызывается успешный обратный вызов, связанный с этой ImageRequest. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.mResponseBitmap = response; // Раздача результатов заблокированных ImageRequest. batchResponse(cacheKey, request); {} {}
Из приведенного выше кода可以看出, после успешного выполнения запроса ImageRequest соответствующий объект BatchedImageRequest уже получен из mInFlightRequests. В то же время, все ImageContainer, соответствующие заблокированным ImageRequest, находятся в集合е mContainers BatchedImageRequest.
Я считаю, что метод batchResponse должен遍ировать только соответствующий集合 mContainers BatchedImageRequest.
Но, на мой взгляд, в исходном коде ImageLoader избыточно создается объект HashMap mBatchedResponses для сохранения集合 BatchedImageRequest, а затем в методе batchResponse выполняются два уровня итераций для遍ирования集合, что очень странно, пожалуйста, дайте указание.
Странный код如下:
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 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); {} {}
•使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了.
自定义L1缓存类
首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:
import android.graphics.Bitmap; import android.support.v4.util.LruCache; /** Lru算法的L1缓存实现类. */ @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); {} {}
Вот и все, что было в этой статье, мы надеемся, что это поможет вам в изучении, и希望大家多多支持呐喊教程。
Заявление: содержимое этой статьи взято из Интернета, авторские права принадлежат соответствующему автору. Содержимое предоставлено пользователями Интернета, самостоятельно загруженным, сайт не имеет права собственности, не был обработан вручную, и не несет ответственности за соответствующие юридические последствия. Если вы обнаружите содержимое,涉嫌侵犯版权, пожалуйста, отправьте письмо по адресу: notice#oldtoolbag.com (во время отправки письма замените # на @) для сообщения о нарушении,并提供 соответствующие доказательства. При подтверждении факта нарушения сайт немедленно удаляет涉嫌侵权的内容.