Android Volleyソース解析(三)、画像ロードの実現

19325 ワード

前言
前の文章では、Volleyのキャッシュメカニズムを深く探究し、ソース分析を通じてキャッシュの動作原理を理解しました.この文章では、「Volleyピクチャロードの実現」を探究します.ピクチャロードとキャッシュは密接に関連しています.まず、Android Volleyソースコード解析(二)、キャッシュメカニズムを探究することをお勧めします.
これはVolleyソース解析シリーズの最後の文章です.今日は基本的な使い方とソース分析を組み合わせた方法で行います.もちろん、本稿のソースは最初のソース分析に基づいています.まだこの文章を見たことがない友达は、まず読むことをお勧めします.Android Volleyソース解析(一)、ネットワークリクエストの実行プロセスです.
一、画像のロードの基本的な使い方
ソース解析を行う前に、Volleyでのピクチャロードに関する基本的な使い方を見てみましょう.
1.1 ImageRequestの使い方
ImageRequestもStringRequestもJsonRequestもRequestから継承されているため、基本的に同じ使い方をしています.まず、RequestQueueオブジェクトを取得する必要があります.
RequestQueue mQueue = Volley.newRequestQueue(context);  

次にnewはImageRequestオブジェクトを出力します.
   private static final String URL = "http://ww4.sinaimg.cn/large/610dc034gw1euxdmjl7j7j20r2180wts.jpg";

   ImageRequest imageRequest = new ImageRequest(URL, new Response.Listener() {
       @Override
       public void onResponse(Bitmap response) {
           imageView.setImageBitmap(response);
       }
   }, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() {
       @Override
       public void onErrorResponse(VolleyError error) {

       }
   });

ImageRequestは6つのパラメータを受信していることがわかります.
1、画像のURLアドレス
2、画像要求の成功したコールバック、ここで私達は戻ったBitmapをImageViewに設定する
3、4はそれぞれ画像の最大許容幅と高さを指定するために用いられ、指定したネットワーク画像の幅または高さがここの値より大きい場合は画像を圧縮し、0に指定すると、画像がどんなに大きくても圧縮しないことを示す
5、画像の属性を指定し、Bitmap.Configの下のいくつかの定数はすべて使用することができて、その中のARGB_8888は最良の色属性を示すことができ、各ピクチャの画素画素は4バイトを占め、RGB_565は、各ピクチャ画素が2バイトを占めることを示す
6、画像要求に失敗したコールバック
最後にこのImageRequestをRequestQueueに追加すればいいです
mQueue.add(imageRequest);

1.2 ImageLoaderの使い方
ImageLoaderは、実際にはImageRequestのパッケージであり、画像をキャッシュするだけでなく、重複するリンクをフィルタリングし、重複送信要求を回避することができるため、ImageLoaderはImageRequestよりも効率的です.
ImageLoaderの使い方は、主に次の4つのステップに分けられます.
1.RequestQueueオブジェクトの作成2、ImageLoaderオブジェクトの作成3、ImageListenerオブジェクトの取得4、ImageLoaderを呼び出すget()メソッド記述ピクチャ
   RequestQueue requestQueue = Volley.newRequestQueue(this);
   ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
       @Override
       public Bitmap getBitmap(String url) {
           return null;
       }

       @Override
       public void putBitmap(String url, Bitmap bitmap) {

       }
   });
   ImageLoader.ImageListener listener = ImageLoader.getImageListener(mIvShow, R.mipmap.ic_launcher, R.mipmap.ic_launcher_round);
   imageLoader.get(URL, listener);

ImageLoaderのコンストラクション関数は2つのパラメータを受信していることがわかります.1つ目のパラメータはRequestQueueオブジェクトで、2つ目のパラメータはImageCacheです.ここでは直接newで空のImageCache実装をすればいいです.
ImageListenerには、ロードするピクチャのURLと、ピクチャプレースホルダと、ロードに失敗した後に表示するピクチャが入力され、最後にImageLoaderが呼び出される.get()メソッドで画像のロードができます.
1.3 NetworkImageView
以上の2つの方法に加えて、Volleyは第3の方法でネットワークピクチャをロードすることを提供し、NetworkImageViewはImageViewから継承されたカスタムViewであり、ImageViewに基づいてネットワークピクチャをロードする機能を拡張する.NetworkImageViewの使い方は簡単です.大きく4つのステップに分けられます.
1、RequestQueueオブジェクトの作成2、ImageLoaderオブジェクトの作成3、コードからNetworkImageViewを取得するインスタンス4、ロードするピクチャアドレスの設定
次のようになります.
   RequestQueue requestQueue = Volley.newRequestQueue(this);
   ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
       @Override
       public Bitmap getBitmap(String url) {
           return null;
       }

       @Override
       public void putBitmap(String url, Bitmap bitmap) {

       }
    });
   networkImageView.setImageUrl(URL, imageLoader);

二、ImageRequestソースコード解析
前節ではVolleyピクチャのロードの3つの方法を紹介しましたが、この節からソースコードを組み合わせてVolleyピクチャのロードの実現を分析し、ImageRequestから始めましょう.
Android Volleyソース解析(一)では,ネットワークリクエストの実行プロセスについて,ネットワークリクエストは最終的にサーバから返された結果をNetworkResponseにカプセル化してRequestに渡して処理すると述べた.ImageRequestの仕事は、NetworkResponseをBitmapを含むResponseに解析し、最後にコールバックすることです.
私たちが分析しなければならないのは、この過程です.
parseNetworkResponseにはdoParse()メソッドが1つしかないことがわかります
    @Override
    protected Response parseNetworkResponse(NetworkResponse response) {
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                return Response.error(new ParseError(e));
            }
        }
    }

doParse()でどのような操作が行われているのか見てみましょう
    private Response doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap = null;
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            // ①    Bitmap       
            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 desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // ③                 Bitmap
            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize =
                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap =
                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // ④    Bitmap    bull              ,     
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap,
                        desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

         // ⑤         Bitmap   Response     
        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }

コードが長いので、5つのステップに分けてみましょう.
①Bitmapオリジナルの幅と高さを取得する
送信NetworkResponseのdataをBitmapFactoryで対応するBitmapに変換し、BitmapOptionsを設定.inJustDecodeBounds=true、Bitmapの元の幅と高さを得て、ここで補足して、BitmapOptions.inJustDecodeBounds=trueのとき、BitmapFactory.decodeは本当にbitmapを返すわけではありません.幅や高さなどの画像のサイズ情報を返すだけで、メモリをあまり消費しません.
②私たちが本当に望んでいる幅と高さを計算する
ImageRequestを構築したときに渡されたパラメータを覚えているでしょう.その6つのパラメータには、画像の最大幅と高さをそれぞれ指定する2つのパラメータが含まれています.私たちは、入力した画像の最大幅と高さ、Bitmapの実際の幅と高さをgetResizedDemension()メソッドで計算し、適切な画像の表示幅を以下のように計算します.
    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
            int actualSecondary, ScaleType scaleType) {

        if ((maxPrimary == 0) && (maxSecondary == 0)) {
            return actualPrimary;
        }

        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 == ScaleType.CENTER_CROP) {
            if ((resized * ratio) < maxSecondary) {
                resized = (int) (maxSecondary / ratio);
            }
            return resized;
        }

        if ((resized * ratio) > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }

③私たちが望む幅と高さに応じて対応するBitmapを得る
DecodeOptions.inJustDecodeBounds=trueは本当のBitmapを返しますDecodeOptions.inSampleSizeはピクチャのサンプリングレートを表し,ピクチャ圧縮に関するパラメータであり,inSampleSize=2であれば元のピクチャの幅と高さをそれぞれ元の1/2に減少させることを表す.
    decodeOptions.inJustDecodeBounds = false;
    decodeOptions.inSampleSize =
        findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
    Bitmap tempBitmap =
        BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
    //         
    static int findBestSampleSize(
            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desiredHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }
        return (int) n;
    }

④Bitmapがbullでなく、目標幅よりも幅または高さが大きい場合は、再度圧縮
   if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
            tempBitmap.getHeight() > desiredHeight)) {
        bitmap = Bitmap.createScaledBitmap(tempBitmap,
                desiredWidth, desiredHeight, true);
        tempBitmap.recycle();
   } else {
        bitmap = tempBitmap;
   }

⑤得られたBitmapを含むResponseをコールバックする
   if (bitmap == null) {
       return Response.error(new ParseError(response));
   } else {
       return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
   }

三、ImageLoaderソースコード解析
ImageLoaderの使い方については、主に4つのステップに分かれています.
1.RequestQueueオブジェクトの作成2、ImageLoaderオブジェクトの作成3、ImageListenerオブジェクトの取得4、ImageLoaderを呼び出すget()メソッドによるピクチャのロード
では、その使い方から始めて、どのように実現されたのかを一歩一歩分析します.
RequestQueueの作成については前述したように、Android Volleyソースコード解析(一)、ネットワークリクエストの実行プロセス、ImageLoaderの構築方法を参照してください.
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
        mRequestQueue = queue;
        mCache = imageCache;
    }

現在のインスタンスのメンバー変数にRequestQueueとImageCacheを割り当てた構造方法が表示されます.次に、ImageListenerを参照して取得します.ImageListenerはImageLoaderを介して取得されます.getImageListener()メソッド:
   public static ImageListener getImageListener(final ImageView view,
            final int defaultImageResId, final int errorImageResId) {
        return new ImageListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                if (errorImageResId != 0) {
                    view.setImageResource(errorImageResId);
                }
            }

            @Override
            public void onResponse(ImageContainer response, boolean isImmediate) {
                if (response.getBitmap() != null) {
                    view.setImageBitmap(response.getBitmap());
                } else if (defaultImageResId != 0) {
                    view.setImageResource(defaultImageResId);
                }
            }
        };
    }

ここでは主にコールバックされたBitmapを対応するImageViewに設定し,いくつかのピクチャロードのフォールトトレランス処理を行うことがわかる.
最後に、ImageLoaderのget()メソッドはImageLoaderクラスで最も複雑なメソッドであり、最も核心的なメソッドでもあるので、一緒に見てみましょう.
    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        //                (UI           )
        throwIfNotOnMainThread();

        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        //           Bitmap,   Bitmap    null,     imageListener   Bitmap     ImageView
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        imageListener.onResponse(imageContainer, true);
 
        //                
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            request.addContainer(imageContainer);
            return imageContainer;
        }

        //               ,         ,
        Request newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);
        mRequestQueue.add(newRequest);
        //        
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

まず現在のスレッドの判断を行い,メインスレッドでなければ直接エラーを投げ出す.
    private void throwIfNotOnMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
        }
    }

次に対応するBitmapをキャッシュから取り出し、Bitmapがnullでない場合はImageListenerに直接コールバックして対応するImageViewにBitmapを設定します.
   Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
   if (cachedBitmap != null) {
       ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
       imageListener.onResponse(container, true);
       return container;
   }

そしてUrlに従ってキャッシュキューからRequestを取り出す
   BatchedImageRequest request = mInFlightRequests.get(cacheKey);   
   if (request != null) {
       request.addContainer(imageContainer);
       return imageContainer;    
   }

キャッシュにリクエストが見つからない場合は、ネットワークリクエストを1回実行します.
   Request newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
           cacheKey);

ImageLoaderがmakeImageReqeust()メソッドを呼び出してRequestを構築していることがわかります.彼がどのように実現しているかを見てみましょう.
    protected Request makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
            ScaleType scaleType, final String cacheKey) {
        return new ImageRequest(requestUrl, new Listener() {
            @Override
            public void onResponse(Bitmap response) {
                onGetImageSuccess(cacheKey, response);
            }
        }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                onGetImageError(cacheKey, error);
            }
        });
    }

ネットワーク要求が成功すると、onGetImageSuccess()メソッドを呼び出し、Bitmapをキャッシュし、キャッシュキュー内のcacheKeyに対応するBatchedImageRequestを削除し、最後にbatchResponse()メソッドを呼び出します.
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
        mCache.putBitmap(cacheKey, response);

        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            request.mResponseBitmap = response;
            batchResponse(cacheKey, request);
        }
    }

batchResponse()メソッドでは、メインスレッドでBitmapをImageListerにコールバックし、BitmapをImageViewに設定することで、ピクチャロードのすべてのプロセスが完了します.
    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;
                }

            };
            mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
        }
    }

四、NetworkImageViewソースコード解析
NetworkImageViewは、内部でImageLoaderを使用してネットワークピクチャをロードするカスタムViewです.NetworkImageViewの使用方法は、主に4つのステップに分かれています.
1.RequestQueueオブジェクトを作成する2、ImageLoaderオブジェクトを作成する3、コードからNetworkImageViewのインスタンスを取得する4、setImageUrl()メソッドを呼び出してロードするピクチャアドレスを設定する
最後のステップはNetworkImageViewのコアです.setImageUrl()内部でどのように実現されているかを見てみましょう.
    public void setImageUrl(String url, ImageLoader imageLoader) {
        mUrl = url;
        mImageLoader = imageLoader;
        loadImageIfNecessary(false);
    }

簡単な3行のコードしかないので、主な論理はloadImageIfNecessary()という方法の中にあると思います.
    void loadImageIfNecessary(final boolean isInLayoutPass) {

        //    URL   null,      
        if (TextUtils.isEmpty(mUrl)) {
            if (mImageContainer != null) {
                mImageContainer.cancelRequest();
                mImageContainer = null;
            }
            setDefaultImageOrNull();
            return;
        }

        //     NetworkImageView         setImageUrl(),
        //       Url        URL     
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                return;
            } else {
                mImageContainer.cancelRequest();
                setDefaultImageOrNull();
            }
        }
        
        //    ImageLoader       
        mImageContainer = mImageLoader.get(mUrl,
                new ImageListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        if (mErrorImageId != 0) {
                            setImageResource(mErrorImageId);
                        }
                    }

                    @Override
                    public void onResponse(final ImageContainer response, boolean isImmediate) {
                        if (isImmediate && isInLayoutPass) {
                            post(new Runnable() {
                                @Override
                                public void run() {
                                    onResponse(response, false);
                                }
                            });
                            return;
                        }

                        if (response.getBitmap() != null) {
                            setImageBitmap(response.getBitmap());
                        } else if (mDefaultImageId != 0) {
                            setImageResource(mDefaultImageId);
                        }
                    }
                }, maxWidth, maxHeight, scaleType);
    }

コードは比較的明確で、まずいくつかのフォールトトレランスの処理を行い、ImageLoaderを呼び出して対応するbitmapを取得し、最後にNetworkImageViewに設定.
まとめ
Volleyソース解析シリーズは、ここまでですべて終わりました.これは私が書いた最も長いシリーズの文章です.最初からVolleyソースの読書から、その後のコード整理と今の文章の出力まで、私は1週間もかかりませんでしたが、ネットロードと画像ロードについてもっと理解しました.ここを完全に見ることができるのはすべて本当の爱ですね.ありがとうございました.
関連記事
  • Android Volleyソースコード解析(一)、ネットワーク要求の実行フロー
  • Android Volleyソースコード解析(二)、キャッシュメカニズム
  • を探究する