Volley框架之四 加載網絡圖片

使用Volley去加載圖片的方式前面已經介紹了,有三種方式,下面主要介紹ImageRequest方式和ImageLoader方式的區別和聯繫

ImageRequest

看一下源代碼,主要就是parseNetworkResponse方法的實現,解析網絡返回的response

    /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
    private static final Object sDecodeLock = new Object();
    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     */
    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        // Serialize all decode on a global lock to reduce concurrent heap usage.
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                return Response.error(new ParseError(e));
            }
        }
    }
    private Response<Bitmap> 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);//圖片的輸入流byte[]數組全部在response.data中,所以這行代碼還是可能出現OOM啊,內存溢出
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;
            // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
            // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
            decodeOptions.inSampleSize =
                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap =
                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap,
                        desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }
  1. parseNetworkResponse解析的時候,加了一把sDecodeLock同步鎖,防止多線程同時解析一張以上圖片造成OOM。同時解析過程中如果出現了OOM,那麼就捕獲OutOfMemoryError這個Error,不讓應用force close,這也跟我們提供了一種思路,如果代碼某處可能出現OOM,那麼可以這樣做。
  2. 可以看到這個方法裏面實現了圖片縮放,通過inJustDecodeBounds設置爲true只獲取圖片寬高,不加載圖片實際數據。根據圖片實際寬高和ImageView設置寬高生成inSampleSize壓縮值,最後inJustDecodeBounds設置爲false,生成bitmap對象
  3. decodeByteArray方法解析bitmap,NetworkResponse.data是個byte[]
  4. getResizedDimension還會根據縮放類型mScaleType來調整圖片尺寸,如果是(0,0)那麼就是原尺寸,不做任何的縮放
  5. ImageRequest繼承自Request,所以自然實現了硬盤緩存的功能
  6. bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); //圖片的輸入流byte[]數組全部在response.data中,所以這行代碼還是可能出現OOM啊,內存溢出

注意點:
tempBitmap.recycle(); 雖然在高版本中recycle方法不會做任何處理,但是爲了兼容低版本,所以調用不使用bitmap的recycle方法是個很好的習慣

缺點:
ImageRequest繼承自Request,所以跟StringRequest等的使用方法差不多,提供了硬盤緩存,同時爲了防止OOM對原圖片進行了縮放處理。
但是另一個問題,
1. ImageRequest不適用於listview,gridview等滑動快速,需要頻繁加載圖片的場景,因爲沒有提供內存緩存,所以每次需要通過緩存線程從硬盤中讀或者網絡請求線程從網絡上拉,這無疑比內存緩存慢太多。
2. 圖片的輸入流byte[]數組全部在response.data中,所以這行代碼還是可能出現OOM啊,內存溢出
這個時候就需要使用ImageLoader咯,相比較ImageRequest,多了內存緩存的功能實現,下面介紹ImageLoader

ImageLoader

用法

//自定義的BitmapCache實現ImageLoader的內部接口ImageCache
public class BitmapCache implements ImageCache {

    private LruCache<String, Bitmap> mCache;

    public BitmapCache() {
        int maxMemory = (int) (Runtime.getRuntime().maxMemory / 1024); //單位kb
        int cacheSize = maxMemory / 8;
        mCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024; //單位kb
            }
        };
    }

    @Override
    public Bitmap getBitmap(String url) {
        return mCache.get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mCache.put(url, bitmap);
    }

}
ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
ImageListener listener = ImageLoader.getImageListener(imageView,
        R.drawable.default_image, R.drawable.failed_image);
imageLoader.get("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg", listener);
//imageLoader.get("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",
                listener, 200, 200);

int cacheSize = maxMemory / 8;把該應用可以的內存的1/8拿出來當緩存,同時另一方面如果應用OOM,那麼可以考慮是否取消該內存緩存,因爲畢竟佔用了1/8的內存。

源碼

    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        // only fulfill requests that were initiated from the main thread.
        throwIfNotOnMainThread();

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

        // Try to look up the request in the cache of remote images.
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // Return the cached bitmap.
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        // The bitmap did not exist in the cache, fetch it!
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // Update the caller to let them know that they should use the default bitmap.
        imageListener.onResponse(imageContainer, true);

        // Check to see if a request is already in-flight.
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // If it is, add this request to the list of listeners.
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // The request is not already in flight. Send the new request to the network and
        // track it.
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);

        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }
    protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
            ScaleType scaleType, final String cacheKey) {
        return new ImageRequest(requestUrl, new Listener<Bitmap>() {
            @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);
            }
        });
    }
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
        // cache the image that was fetched.
        mCache.putBitmap(cacheKey, response);

        // remove the request from the list of in-flight requests.
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            // Update the response bitmap.
            request.mResponseBitmap = response;

            // Send the batched response
            batchResponse(cacheKey, request);
        }
    }

首先調用throwIfNotOnMainThread(); 說明不能在子線程調用get方法,這個很顯然的,子線程不能更新view嘛,但是ImageRequest沒有對子線程進行這個處理,因爲直接繼承子Request嘛,其它的request不需要更新view,也就是沒有子線程還是主線程調用的限制。如果在子線程中通過ImageRequest加載ImageView的話,必須會froce close。這是第一個區別

    public interface ImageCache {
        public Bitmap getBitmap(String url);
        public void putBitmap(String url, Bitmap bitmap);
    }
    final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    // Try to look up the request in the cache of remote images.
    Bitmap cachedBitmap = mCache.getBitmap(cacheKey);

ImageCache是個接口,上面代碼中BitmapCache就是實現了這個接口,內部使用LRUCache。首先看看ImageCache中是否有,有的話直接拿過來返回,沒有的話,通過ImageRequest發起請求,或者從硬盤緩存,或者從網絡。makeImageRequest可以看到內部使用了ImageRequest發起請求,同時如果請求成功回調onGetImageSuccess方法,mCache.putBitmap(cacheKey, response);加入mCache,所以如果下次再請求的話,如果還再mCache中,就直接拿了,這樣就實現了內存緩存。

因爲內部實現是是ImageRequest,所以也會有圖片的輸入流byte[]數組全部在response.data中,所以這行代碼還是可能出現OOM啊,內存溢出

另一方面,爲什麼不用一個Map保存所有圖片的軟引用或者弱引用呢,獲取的時候去這個Map中取,這樣不是也可以實現內存緩存的目的嗎,爲什麼最好還是使用LruCache呢?
Android用LruCache來取代原來強引用和軟引用實現內存緩存,因爲自2.3以後Android將更頻繁的調用GC,導致軟引用緩存的數據極易被釋放,所以是不可靠的。
現在知道了原因了吧?如果使用軟引用/弱引用,那麼發生GC的時候,那麼Map保存的bitmap都被回收了,所以不合適。。
現在來分析一下LruCache的原理

LruCache原理

LRUCache(Least Recently Used 近期最少使用)
無外乎就是分析get和put方法

LruCache使用一個LinkedHashMap簡單的實現內存的緩存,沒有軟引用,都是強引用。如果添加的數據大於設置的最大值,就刪除最先緩存的數據來調整內存。他的主要原理在trimToSize方法中。需要了解兩個主要的變量size和maxSize。
maxSize是通過構造方法初始化的值,他表示這個緩存能緩存的最大值是多少。
size在添加和移除緩存都被更新值,他通過safeSizeOf這個方法更新值。safeSizeOf默認返回1,但一般我們會根據maxSize重寫這個方法,比如認爲maxSize代表是KB的話,那麼就以KB爲單位返回該項所佔的內存大小。除異常外首先會判斷size是否超過maxSize,如果超過了就取出最先插入的緩存,如果不爲空就刪掉(一般來說只要map不爲空都不會返回null,因爲他是個雙休鏈表),並把size減去該項所佔的大小。這個操作將一直循環下去,直到size比maxSize小或者緩存爲空。

    public final V get(K key) {
        ..........
        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }
   /**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    public void trimToSize(int maxSize) {
        while (true) {
            ............
            synchronized (this) {
                ............
                //把map.eldest()節點remove掉
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
    //LinkHashMap的eldest方法,其實就是把LinkHashMap的頭拿出來
    public Entry<K, V> eldest() {
        LinkedEntry<K, V> eldest = header.nxt;
        return eldest != header ? eldest : null;
    }

只要get了,那麼說明客戶端就使用到了,看get方法的代碼,內部不止簡單的從map中get,還調用了put方法和trimToSize方法去調整map,此時說明如果get了那麼這項就一定不會是LinkHashMap的頭了。。所以下次要remove的時候,就不會remove這項啦,LRUCache(Least Recently Used 近期最少使用),所以最近使用的肯定不會被remove掉的。

put方法就不介紹拉,其實也就是如果超過了LRUCache的maxSize,那麼最遠添加的就會被remove掉

最佳實踐

首先執行以下命令 adb shell setprop log.tag.Volley VERBOSE


第二次請求圖片,那麼直接從硬盤緩存中讀取,此時沒使用內存緩存,entry.isExpired()==false

fiddler抓包,注意max-age字段,volley計算ttl的時候會使用這個字段

發佈了78 篇原創文章 · 獲贊 9 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章