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; /** Verrou de synchronisation de l'analyse de Bitmap, garantissant qu'un seul Bitmap soit chargé et analysé en mémoire à chaque fois pour éviter l'OOM. */ private static final Object sDecodeLock = new Object(); /** * Construire une demande d'image réseau. * @param url Adresse URL de l'image. * @param listener Interface de rappel utilisateur définie pour les succès de demande. * @param maxWidth Largeur maximale de l'image. * @param maxHeight Hauteur maximale de l'image. * @param scaleType Type de mise à l'échelle de l'image. * @param decodeConfig Configuration d'analyse du bitmap. * @param errorListener Interface de rappel utilisateur définie pour les erreurs de demande. */ 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; } /** Définir le niveau de priorité de la demande d'image réseau. */ @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 { // Obtenir la taille réelle de l'image réseau. 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; } /** set the size of the image according to the ScaleType of ImageView. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ImageView.ScaleType scaleType) {}} // Si ImageView ne définit pas la valeur maximale, retourner directement la taille réelle de l'image réseau. if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } // Si le ScaleType de ImageView est FIX_XY, le mettre en valeur le plus élevé de l'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); } }
Étant donné que le cadre Volley a déjà implémenté le cache local pour les demandes réseau, la principale tâche de ImageRequest consiste à analyser le flux de bytes en Bitmap, et pendant le processus d'analyse, il utilise des variables statiques pour garantir que chaque Bitmap est analysé une seule fois pour éviter l'OOM, et il utilise ScaleType et les valeurs MaxWidth et MaxHeight définies par l'utilisateur pour définir la taille de l'image.
En résumé, l'implémentation de ImageRequest est très simple, et ici, nous ne donnerons pas de détails supplémentaires. Les faiblesses de ImageRequest résident dans :
1. Nécessite de nombreuses configurations de l'utilisateur, y compris la taille maximale de l'image.
2. Pas de cache en mémoire d'image, car le cache de Volley est basé sur Disk, il y a un processus de désérialisation d'objet.
ImageLoader.java
En raison de ces deux缺点,Volley a fourni une classe ImageLoader encore plus formidable. Parmi eux, le plus crucial est l'ajout de cache en mémoire.
Avant d'expliquer le code source de ImageLoader, il faut d'abord présenter l'utilisation de ImageLoader. Contrairement aux Request de l'ancien, ImageLoader n'est pas directement newé et jeté dans RequestQueue pour la planification, sa méthode d'utilisation est principalement divisée en4Étape:
• Créer un objet RequestQueue.
RequestQueue queue = Volley.newRequestQueue(context);
• Créer un objet ImageLoader.
Le constructeur de ImageLoader prend deux paramètres, le premier est l'objet RequestQueue, le second est l'objet ImageCache (c'est-à-dire la classe de cache en mémoire, nous ne donnerons pas d'implémentation spécifique pour le moment, après avoir expliqué le code source de ImageLoader, je fournirai une implémentation de ImageCache utilisant l'algorithme LRU)
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() { @Override public void putBitmap(String url, Bitmap bitmap) {} @Override public Bitmap getBitmap(String url) { return null; } });
• Obtenir un objet ImageListener.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
• Appel de la méthode get de ImageLoader pour charger l'image réseau.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
L'utilisation de ImageLoader, combinée avec la méthode d'utilisation, nous allons examiner le code source de ImageLoader:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * lié à la RequestQueue utilisée pour appeler ImageLoader. */ private final RequestQueue mRequestQueue; /** implémentation de l'interface de cache en mémoire des images. */ private final ImageCache mCache; /** stocke la collection de BatchedImageRequest de même CacheKey exécutée en même temps. */ private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<String, BatchedImageRequest>(); private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>(); /** obtient le Handler du thread principal. */ private final Handler mHandler = new Handler(Looper.getMainLooper()); private Runnable mRunnable; /** définir l'image K1interface de cache, qui transfère le travail de cache en mémoire des images à l'utilisateur pour l'implémenter. */ public interface ImageCache { Bitmap getBitmap(String url); void putBitmap(String url, Bitmap bitmap); } /** constructe un ImageLoader. */ public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; } /** constructe l'interface de rappel de succès et d'échec de la demande d'image réseau. */ 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) { // Juger si la méthode actuelle s'exécute dans le thread UI. Si ce n'est pas le cas, lève une exception. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // À partir de L1Obtenir le Bitmap correspondant en fonction de la clé dans le cache de niveau. Bitmap cacheBitmap = mCache.getBitmap(cacheKey); if (cacheBitmap != null) { // L1Si la correspondance en cache est réussie, construire ImageContainer à partir du Bitmap de la correspondance en cache et appeler l'interface de succès de la réponse de imageListener. ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null); // Attention : car nous sommes actuellement dans le thread UI, c'est ici que nous appelons la méthode onResponse, pas la méthode de rappel. imageListener.onResponse(container, true); return container; } ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // L1Si la correspondance en cache échoue, il faut d'abord définir l'image par défaut pour l'ImageView. Ensuite, utiliser un sous-thread pour télécharger l'image réseau et l'afficher. imageListener.onResponse(imageContainer, true); // Vérifiez si la demande d'ImageRequest correspondante de cacheKey est en cours d'exécution. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // Si un ImageRequest identique est en cours d'exécution, il n'est pas nécessaire de lancer un ImageRequest identique en même temps. // Il suffit d'ajouter l'ImageContainer correspondant à la collection mContainers de BatchedImageRequest. // Après la fin de l'ImageRequest en cours d'exécution, il regarde combien d'ImageRequest bloquants sont en cours d'exécution actuellement, // Puis il appelle les retours de la collection mContainers. request.addContainer(imageContainer); return imageContainer; } // L1Le cache n'a pas réussi à correspondre, il est nécessaire de construire ImageRequest, et de récupérer l'image réseau par le biais de la planification de RequestQueue // La méthode d'obtention peut être :2Cache (note : cache disque) ou requête HTTP. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; } /** Construct L1La valeur de clé de cache. */ 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); } }); } /** Rappel d'échec de la demande d'image. Exécuté sur le thread UI. */ private void onGetImageError(String cacheKey, VolleyError error) { BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { request.setError(error); batchResponse(cacheKey, request); } } /** Rappel de succès de la demande d'image. Exécuté sur le thread 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 doit être invoqué depuis le thread principal."); } } /** Abstraction de l'interface de rappel de succès et d'échec de la demande. Par défaut, vous pouvez utiliser l'ImageListener fourni par Volley. */ public interface ImageListener extends Response.ErrorListener { void onResponse(ImageContainer response, boolean isImmediate); } /** L'objet porteur de la demande d'image réseau. */ public class ImageContainer { /** Le Bitmap à charger pour l'ImageView. */ private Bitmap mBitmap; /** L1La clé de cache */ private final String mCacheKey; /** L'URL de la demande ImageRequest. */ private final String mRequestUrl; /** L'interface de rappel de succès ou d'échec de la demande d'image. */ 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; } } /** * Classe abstraite de requête ImageRequest avec le même CacheKey. * La détermination de deux ImageRequest identiques inclut : * 1. L'URL est la même. * 2. maxWidth et maxHeight sont identiques. * 3. Le même scaleType est affiché. * Il peut y avoir plusieurs requêtes ImageRequest avec le même CacheKey en même temps, en raison du fait que tous les Bitmap à renvoyer sont identiques, on utilise donc BatchedImageRequest * pour réaliser cette fonction. Une ImageRequest avec le même CacheKey ne peut y avoir qu'une seule en même temps. * Pourquoi ne pas utiliser la mWaitingRequestQueue de RequestQueue pour réaliser cette fonction? * Réponse : Ce n'est pas possible de déterminer si deux ImageRequest sont égaux uniquement à partir de l'URL. */ private class BatchedImageRequest { /** La requête ImageRequest correspondante. */ private final Request<?> mRequest; /** L'objet Bitmap du résultat de la requête. */ private Bitmap mResponseBitmap; /** Erreur de ImageRequest. */ private VolleyError mError; /** Collection d'encapsulation des résultats de requêtes ImageRequest identiques. */ 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; } } }
Grande question
Je possède deux grandes questions sur le code source d'Imageloader :63;
•implémentation de la méthode batchResponse.
Je suis étonné, pourquoi le classe ImageLoader doit inclure une HashMap pour stocker la collection BatchedImageRequest ?63;
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
Après tout, batchResponse est appelé dans le rappel de succès d'une ImageRequest spécifique, le code d'appel est le suivant :
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); } }
Voici la fin de l'article. J'espère que cela pourra aider à votre apprentissage et que vous serez nombreux à soutenir le tutoriel Yelling.
Déclaration : le contenu de cet article est hébergé sur Internet, propriété de ses auteurs respectifs, le contenu est contribué et téléversé par les utilisateurs d'Internet, ce site ne détient pas de droits de propriété, n'a pas été édité par l'homme, et n'assume aucune responsabilité juridique connexe. Si vous trouvez du contenu présumé de violation de copyright, veuillez envoyer un e-mail à : notice#oldtoolbag.com (veuillez remplacer # par @ lors de l'envoi d'un e-mail pour signaler une violation, et fournir des preuves pertinentes. Une fois confirmée, ce site supprimera immédiatement le contenu présumé de violation de droit d'auteur.)