English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Gituhbプロジェクト
Volleyのソースコードの中国語コメントプロジェクトはgithubにアップロードしました。forkとstarをお待ちしています。
このブログを書く理由
この記事は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の解析を同期するロックオブジェクト、同じ時間に1つのBitmapがメモリにloadされ解析されることを保証し、OOMを防ぎます。 */ private static final Object sDecodeLock = new Object(); /** * ネットワーク画像リクエストを構築します。 * @param url 画像のURLアドレス。 * @param listener リクエスト成功時のユーザー設定の回调インターフェース。 * @param maxWidth 画像の最大幅。 * @param maxHeight 画像の最大高さ。 * @param scaleType 画像のスケール変更タイプ。 * @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); mListener = listener; mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; mScaleType = scaleType; } /** ネットワーク画像リクエストの優先度を設定します。 */ @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 { // ネットワーク画像の実際のサイズを取得します。 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に応じて画像のサイズを設定します. */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ImageView.ScaleType scaleType) {}} // ImageViewの最大値が設定されていない場合、ネットワーク画像の実際のサイズを直接返します。 if ((maxPrimary == 0) && (maxSecondary == 0)) { return actualPrimary; } // ImageViewのScaleTypeが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に解析し、解析中に静的変数を使用して一度に1つのBitmapのみを解析することでOOMを防ぎ、ScaleTypeとユーザーが設定したMaxWidthとMaxHeightを使用して画像のサイズを設定します。
全体的に見て、ImageRequestの実装は非常にシンプルであり、詳細な説明は省略します。ImageRequestの欠点は以下の通りです:
1.ユーザーが多くの設定を行う必要があります。画像の大きさの最大値を含めます。
2.メモリキャッシュはありません。Volleyのキャッシュはディスクキャッシュに基づいており、オブジェクトの反序列化プロセスがあります。
ImageLoader.java
以上の2つの欠点を克服するために、Volleyはより強力なImageLoaderクラスを提供しました。その中で最も重要なのはメモリキャッシュの追加です。
ImageLoaderのソースコードを説明する前に、まずImageLoaderの使用方法について説明します。前のRequestリクエストとは異なり、ImageLoaderは直接RequestQueueに投げ込むのではなく、使用方法は大別して以下の通りです。4歩:
• RequestQueueオブジェクトを生成します。
RequestQueue queue = Volley.newRequestQueue(context);
• ImageLoaderオブジェクトを生成します。
ImageLoaderのコンストラクタは2つの引数を受け取ります。1つ目はRequestQueueオブジェクト、2つ目はImageCacheオブジェクトです(つまりメモリキャッシュクラスです。詳細な実装はここでは示しませんが、ImageLoaderのソースコードを説明した後、LRUアルゴリズムを使用するImageCacheの実装クラスを提供します)。
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);
• ImageLoaderのgetメソッドを呼び出してネットワーク画像をロードします。
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
ImageLoaderの使用方法を説明した後、その使用方法と合わせてImageLoaderのソースコードを見てみましょう。
@SuppressWarnings({"unused", "StringBufferReplaceableByString"}) public class ImageLoader { /** * ImageLoaderを呼び出すために使用されるRequestQueueを関連付け. */ private final RequestQueue mRequestQueue; /** 画像メモリキャッシュインターフェース実装クラス. */ private final ImageCache mCache; /** 同一時刻に実行される同じCacheKeyのBatchedImageRequestコレクションを保存. */ 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 get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight, ScaleType scaleType) { // 現在のメソッドがUIスレッド内で実行されているかどうかを判断します。もしでない場合は、例外を投げます。 throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); // 从L1キーに基づいてレベルキャッシュから対応するBitmapを取得します。 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); // 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:ディスクキャッシュ)または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); } }); } /** 画像リクエスト失敗のカールバック。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; } }; // Runnableをポストします mHandler.postDelayed(mRunnable, 100); } } private void throwIfNotOnMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ImageLoaderはメインスレッドから呼び出される必要があります。"); } } /** リクエスト成功および失敗のカールバックインターフェースを抽象化します。デフォルトではVolleyが提供するImageListenerを使用できます。 */ public interface ImageListener extends Response.ErrorListener { void onResponse(ImageContainer response, boolean isImmediate); } /** ネットワーク画像リクエストのキャリアオブジェクト. */ public class ImageContainer { /** ImageViewが読み込む必要があるBitmap. */ private Bitmap mBitmap; /** L1キャッシュのキー */ private final String mCacheKey; /** ImageRequestリクエストのURL. */ 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; } } /** * CacheKeyが同じImageRequestリクエストの抽象クラス。 * 二つのImageRequestが同じと判断するには、以下を含みます: * 1. urlが同じ. * 2. maxWidthとmaxHeightが同じ. * 3. 表示するscaleTypeが同じ. * 同一時間に同じCacheKeyのImageRequestが複数ある場合、返されるBitmapがすべて同じであるため、BatchedImageRequestを使用します。 * この機能を実現するために。同一時間に同じCacheKeyのImageRequestは一つだけです。 * なぜRequestQueueのmWaitingRequestQueueを使用してこの機能を実現しないのですか?63; * 答え: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<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; } } }
大きな疑問
Imageloaderのソースコードについての二つの大きな疑問があります。63;
•batchResponseメソッドの実装。
私は不思議に思っています、なぜImageLoaderクラスにはBatchedImageRequestコレクションを保存するためのHashMapがあるのか?63;
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请求成功后,已经从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; } }; // 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; } }; // Runnableをポストします mHandler.postDelayed(mRunnable, 100); } }
•ImageLoaderがデフォルトで提供するImageListenerを使用すると、画像が一時的に表示される問題があります。ListViewのアイテムに画像を設定する場合、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(メールを送信する際には、#を@に置き換えてください。報告を行い、関連する証拠を提供してください。一旦確認がついたら、このサイトは侵害を疑う内容をすぐに削除します。)