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的时候会使用这个字段

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值