English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Gituhb项目
Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.
为什么写这篇博客
本来文章是维护在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; /** Bloqueo de sincronización de decodificación de Bitmap, garantiza que solo un Bitmap se cargue y se decodifique en la memoria a la vez, evitando OOM. */ private static final Object sDecodeLock = new Object(); /** * construir una solicitud de imagen de red. * @param url Dirección URL de la imagen. * @param listener Interfaz de devolución de llamada del usuario para solicitudes exitosas. * @param maxWidth Ancho máximo de la imagen. * @param maxHeight Altura máxima de la imagen. * @param scaleType Tipo de escalado de la imagen. * @param decodeConfig Configuración de decodificación de bitmap. * @param errorListener Interfaz de devolución de llamada del usuario para errores de solicitud. */ 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; } /** establecer la prioridad de la solicitud de imagen de red. */ @Override public Priority getPriority() { return Priority.LOW; } @Override protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) { synchronized (sDecodeLock) { 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 { // Obtener el tamaño real de la imagen de la red. 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; } /** According to the ScaleType of ImageView, set the size of the image. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ImageView.ScaleType scaleType) {}} // Si no se han configurado los valores máximos de ImageView, devuelva directamente el tamaño real de la imagen de red. if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } // Si el ScaleType de ImageView es FIX_XY, configúrelo como el valor máximo de la imagen. 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); } }
Debido a que el framework Volley ya implementa el almacenamiento en caché local para solicitudes de red, la principal tarea de ImageRequest es analizar el flujo de bytes en un Bitmap, y durante el proceso de análisis, se asegura de que se analice solo un Bitmap cada vez para evitar la sobrecarga de memoria (OOM), configurando el tamaño de la imagen utilizando ScaleType y los MaxWidth y MaxHeight configurados por el usuario.
En resumen, la implementación de ImageRequest es muy sencilla; aquí no se realizará una explicación detallada. Los defectos de ImageRequest radican en:
1.Requiere que el usuario realice muchos ajustes, incluyendo el tamaño máximo de las imágenes.
2.No hay caché en memoria de imágenes, ya que la caché de Volley se basa en Disk y hay un proceso de deserialización de objetos.
ImageLoader.java
Dado estos dos defectos, Volley proporciona una clase ImageLoader más genial. Lo más crucial es que se ha añadido la caché en memoria.
Antes de explicar el código fuente de ImageLoader, es necesario presentar primero el método de uso de ImageLoader. A diferencia de las solicitudes Request anteriores, ImageLoader no se crea directamente y se tira a RequestQueue para la programación, su método de uso se divide大致 en:4Paso:
•Crear un objeto RequestQueue.
RequestQueue queue = Volley.newRequestQueue(context);
•Crear un objeto ImageLoader.
El constructor de ImageLoader recibe dos parámetros, el primero es el objeto RequestQueue y el segundo es el objeto ImageCache (es decir, la clase de caché en memoria, que no daremos una implementación específica ahora, sino que después de explicar el código fuente de ImageLoader, proporcionaré una clase de implementación de ImageCache utilizando el algoritmo LRU)
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() { @Override public void putBitmap(String url, Bitmap bitmap) {} @Override public Bitmap getBitmap(String url) { return null; } });
•Obtener un objeto ImageListener.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
•Cargar la imagen de red utilizando el método get de ImageLoader.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
Con el uso de ImageLoader, veamos ahora el código fuente de ImageLoader utilizando los métodos de uso:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * asociar la RequestQueue utilizada para llamar a ImageLoader. */ private final RequestQueue mRequestQueue; /** clase de implementación de interfaz de caché de imagen en memoria. */ private final ImageCache mCache; /** almacenar la colección de BatchedImageRequest con el mismo CacheKey ejecutándose al mismo tiempo. */ private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<String, BatchedImageRequest>(); private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>(); /** obtener el Handler de la línea principal. */ private final Handler mHandler = new Handler(Looper.getMainLooper()); private Runnable mRunnable; /** definir la imagen K1interfaz de caché, que delega la tarea de caché de imagen en memoria al usuario. */ public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); } /** constructar un ImageLoader. */ public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; } /** constructar el interfaz de retroalimentación de éxito y fracaso de la solicitud de imagen de red. */ 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) { // Determinar si el método actual se ejecuta en el hilo de la interfaz de usuario. Si no es así, se lanza una excepción. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // Desde L1Obtener el Bitmap correspondiente según la clave en el nivel de caché. Bitmap cacheBitmap = mCache.getBitmap(cacheKey); if (cacheBitmap != null) { // L1Si se encuentra un acierto en la caché, se construye ImageContainer con el Bitmap del acierto en la caché y se llama al interfaz de respuesta exitosa de imageListener. ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null); // Atención: De momento se está ejecutando en el hilo de la interfaz de usuario, por lo que aquí se llama al método onResponse, no a la devolución de llamada. imageListener.onResponse(container, true); return container; } ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // L1Si falla el acierto en la caché, primero se debe configurar la imagen predeterminada de ImageView. Luego, se debe extraer la imagen de la red en un subproceso y mostrarla. imageListener.onResponse(imageContainer, true); // Verifica si la solicitud de ImageRequest correspondiente a cacheKey está en ejecución. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // No es necesario ejecutar simultáneamente ImageRequests idénticos que ya están en ejecución. // Solo se necesita agregar el ImageContainer correspondiente a la colección mContainers de BatchedImageRequest. // Después de que finalice el ImageRequest en ejecución, se verifica cuántos ImageRequest en espera hay. // Luego realiza una devolución de llamada sobre la colección mContainers. request.addContainer(imageContainer); return imageContainer; } // L1No se encontró la caché, aún es necesario construir ImageRequest, para obtener la imagen de red a través de la programación de RequestQueue. // El método de obtención puede ser:L2缓存(ads:Almacenamiento en disco) o solicitud de red 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; } /** 当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); } }); } /** La llamada de retorno de llamada de fracaso de solicitud de imagen. Se ejecuta en el hilo UI. */ private void onGetImageError(String cacheKey, VolleyError error) { BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.setError(error); batchResponse(cacheKey, request); } } /** La llamada de retorno de llamada de éxito de solicitud de imagen. Se ejecuta en el hilo 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 debe ser invocado desde el hilo principal."); } } /** Abstracte la interfaz de callback de éxito y fracaso de la solicitud. Por defecto, se puede utilizar el ImageListener proporcionado por Volley. */ public interface ImageListener extends Response.ErrorListener { void onResponse(ImageContainer response, boolean isImmediate); } /** El objeto portador de solicitud de imagen en red. */ public class ImageContainer { /** El Bitmap que necesita cargar ImageView. */ private Bitmap mBitmap; /** L1La clave de caché. */ private final String mCacheKey; /** La URL de solicitud de ImageRequest. */ private final String mRequestUrl; /** La interfaz de callback de éxito o fracaso de la solicitud de imagen. */ 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; } } /** * Clase abstracta de solicitud ImageRequest con el mismo CacheKey. * Determinar si dos ImageRequest son iguales incluye: * 1. La URL es la misma. * 2. maxWidth y maxHeight son los mismos. * 3. El tipo de escala de visualización es el mismo. * Puede haber múltiples solicitudes ImageRequest con el mismo CacheKey en el mismo tiempo, ya que todos los Bitmap que se deben devolver son iguales, por lo que se utiliza BatchedImageRequest * para implementar esta función. En el mismo tiempo, solo puede haber un ImageRequest con el mismo CacheKey. * ¿Por qué no se utiliza la mWaitingRequestQueue de RequestQueue para implementar esta función? * Respuesta: es porque no se puede determinar si dos ImageRequest son iguales solo con la URL. */ private class BatchedImageRequest { /** Solicitud ImageRequest correspondiente. */ private final Request<?> mRequest; /** Objeto Bitmap del resultado de la solicitud. */ private Bitmap mResponseBitmap; /** Error en ImageRequest. */ private VolleyError mError; /** colección encapsulada de resultados de solicitudes ImageRequest idénticas. */ 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; } } }
dudas importantes
Tengo dos grandes dudas sobre el código fuente de Imageloader.63;
•implementación del método batchResponse.
Me sorprende por qué la clase ImageLoader tiene que tener un HashMap para guardar la colección de BatchedImageRequest.63;
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
Después de todo, batchResponse se llama en el callback de éxito de una ImageRequest específica, el código de llamada es el siguiente:
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请求成功后,已经从mInFlightRequests中获取了对应的BatchedImageRequest对象。而同一时间被阻塞的相同的ImageRequest对应的ImageContainer都在BatchedImageRequest的mContainers集合中。
我认为,batchResponse方法只需要遍历对应BatchedImageRequest的mContainers集合即可。
但是,在ImageLoader源码中,我认为多余地构造了一个HashMap对象mBatchedResponses来保存BatchedImageRequest集合,然后在batchResponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导。
代码如下:
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); } }
Esto es todo el contenido de este artículo, espero que sea útil para su aprendizaje y que todos nos apoyen en el tutorial de alarido.
Declaración: El contenido de este artículo se ha obtenido de la red, pertenece a los propietarios originales, el contenido se ha contribuido y subido por los usuarios de Internet de manera autónoma. Este sitio no posee los derechos de propiedad, no ha sido editado por humanos y no asume ninguna responsabilidad legal relacionada. Si encuentra contenido sospechoso de infracción de derechos de autor, por favor envíe un correo electrónico a: notice#oldtoolbag.com (al enviar un correo electrónico, reemplace # con @) para denunciar, y proporcione evidencia relevante. Una vez confirmado, este sitio eliminará inmediatamente el contenido sospechoso de infracción.