android网络开源框架volley(三)——谈谈图片加载

过完年,心情状态好了很多,终于可以静下心来写点东西。之前关于volley用过的东西都写了,这次再整理下——关于volley加载图片的用法。


1、加载图片,然后显示

加载图片,这个几乎每个app都会有所涉及,然后就是如何流畅加载,如何管理好内存,避免OOM之类。这些东西,几乎是每个程序员必须面对的。使用volley加载网络图片,主要用到其中的ImageLoader类。关于这个类的主要说明如下:


它主要是帮我们载入和缓存从远程网络加载的图片。构造方法中我们需要传入请求队列和一个ImageCache接口的实现(这个地方谷歌并没有为我们做好图片的缓存,我们需要按照自己的思路去实现这些功能,比如LRU,LFU,FIFO等,下面的demo我用的就是support-v4中的Lru),可以做L1 Cache。另外,getImageListener(ImageView view, int defaultImageResId, int errorImageResId)为我们提供了默认的ImageLoader.ImageListener实现。还有一个注意事项:所有的请求都必须在主线程中发出。

ImageLoader提供了两个get方法,具体的实现可以查看源码,值得注意的地方是,get(java.lang.String requestUrl, ImageLoader.ImageListener imageListener, int maxWidth, int maxHeight)这个方法中,我们可以通过设置最大宽高来限制加载到内存中的图片的大小,减少OOM的发生,当加载一些大图片时,效果还是非常明显的。

下面贴一个通过GridView加载图片的代码,首先是adapter:

/**
 * GridView的adapter
 * 
 * @author ttdevs http://blog.csdn.net/ttdevs
 */
public class GridViewAdapter extends BaseAdapter implements OnScrollListener {

	private final int WITDH = 960, HEIGHT = 960; // 默认加载图片的size,如果不设置可能会出现OOM,设置了,就会好一些

	public final static String[] URLS = ImageURLs.imageThumbUrls;

	private Context mContext;
	private ImageGridView mIgvImage;
	private ImageLoader mImageLoader;

	public GridViewAdapter(Context context, ImageGridView igvImage) {
		mContext = context;
		mIgvImage = igvImage;
		mIgvImage.setOnScrollListener(this);

		mImageLoader = VolleyQueue.getImageLoader();
	}

	@Override
	public int getCount() {
		return URLS.length;
	}

	@Override
	public Object getItem(int position) {
		return URLS[position];
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view = null;
		if (convertView == null) {
			view = LayoutInflater.from(mContext).inflate(R.layout.item_image, null);
		} else {
			view = convertView;
		}

		String imageUrl = URLS[position];
		ImageView ivImage = (ImageView) view.findViewById(R.id.ivImage);
		ivImage.setTag(imageUrl);// 给ImageView设置一个Tag,保证异步加载图片时不会乱序

		return view;
	}

	private int mFirstVisibleItem; // 第一张可见图片的下标
	private int mVisibleItemCount; // 一屏有多少张图片可见
	private boolean isFirstEnter = true; // 记录是否刚打开程序,用于解决进入程序不滚动屏幕,不会下载图片的问题。
	private List<ImageContainer> icList = new ArrayList<ImageContainer>();

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
		mFirstVisibleItem = firstVisibleItem;
		mVisibleItemCount = visibleItemCount;

		if (isFirstEnter && visibleItemCount > 0) {
			loadBitmaps(firstVisibleItem, visibleItemCount);
			isFirstEnter = false;
		}
	}

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		// 仅当GridView静止时才去下载图片,GridView滑动时取消所有正在下载的任务
		if (scrollState == SCROLL_STATE_IDLE) {
			loadBitmaps(mFirstVisibleItem, mVisibleItemCount);
		} else {
			for (ImageContainer ic : icList) {
				ic.cancelRequest();
				System.err.println(">>>>> cancel loading:" + ic.getRequestUrl());
			}
			icList.clear();
		}
	}

	private void loadBitmaps(int firstVisibleItem, int visibleItemCount) {
		try {
			for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
				final String imageUrl = URLS[i];

				// // 使用ImageLoader的默认实现
				final ImageView ivImage = (ImageView) mIgvImage.findViewWithTag(imageUrl);
				// ImageListener listener =
				// ImageLoader.getImageListener(ivImage,
				// R.drawable.ic_empty_photo, R.drawable.ic_empty_photo);
				// mImageLoader.get(imageUrl, listener);

				// 自己写的,可能写的不太好,但是这个更有利于理解ImageLoader执行过程
				ImageContainer ic = mImageLoader.get(imageUrl, new ImageListener() {

					@Override
					public void onResponse(ImageContainer response, boolean isImmediate) {
						String imageUrl = response.getRequestUrl();

						if (ivImage != null) {
							Bitmap tbm = response.getBitmap();
							if (tbm != null) {
								System.out.println("<<<<<loading finish:" + imageUrl);
								ivImage.setImageBitmap(response.getBitmap());
							} else {
								ivImage.setImageResource(R.drawable.ic_empty_photo);
							}
						}
					}

					@Override
					public void onErrorResponse(VolleyError error) {
						error.printStackTrace();
						ivImage.setImageResource(R.drawable.ic_empty_photo);
					}

				}, WITDH, HEIGHT); // 此处使用另外一个构造函数在加载的时候是加载原始图片
				System.out.println(">>>>><" + i + ">loading:" + imageUrl);
				icList.add(ic);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

另外需要关注的就是做为处理图片缓存的实现,这里使用LRU策略:

public class BitmapLruCache extends LruCache<String, Bitmap> implements ImageCache {
	
	public BitmapLruCache(int maxSize) {
		super(maxSize);
		initLocalFileManager();
	}

	private void initLocalFileManager() {
		
	}

	@Override
	protected int sizeOf(String key, Bitmap value) {
		// TODO value.getByteCount();
		return value.getRowBytes() * value.getHeight();
	}

	@Override
	public Bitmap getBitmap(String url) {
		Bitmap tbm = get(url);
		if(tbm != null){
			return tbm;
		}
		return null; //TODO local file
	}

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

最后一个和之前文章中区别不大的VolleyQueue.java:

public class VolleyQueue {
	private static RequestQueue mRequestQueue;
	private static ImageLoader mImageLoader;

	private VolleyQueue() {

	}

	/**
	 * 初始话我们的请求队列。这个地方有一个BitmapLruCache,这个在后面做图片加载的时候会提到的图片缓存策略
	 * 
	 * @param context
	 */
	static void init(Context context) {
		mRequestQueue = Volley.newRequestQueue(context);

		int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
		// Use 1/8th of the available memory for this memory cache.
		int cacheSize = 1024 * 1024 * memClass / 8;
		mImageLoader = new ImageLoader(mRequestQueue, new BitmapLruCache(cacheSize));
	}
	......
}

最后看一下运行效果图:


滑动还是非常之流畅的。


2、使用NetworkImageView

有了上面的分析,看这个就比较简单了,就是对ImageView的再封装,加入网络请求:

这个的缺点就是加载大量图片的时候可能更容易出现OOM问题,因为它没有处理图片的压缩,当然,你能保证你的图片很小或者你也可以重写NetworkImageView,给它添加一个设置加载尺寸的方法。


3、分析:ImageLoader和ImageRequest

下面来分析下这个ImageLoader和ImageRequest。首先是ImageLoader,它的关键方法是:
/**
     * Issues a bitmap request with the given URL if that image is not available
     * in the cache, and returns a bitmap container that contains all of the data
     * relating to the request (as well as the default image if the requested
     * image is not available).
     * @param requestUrl The url of the remote image
     * @param imageListener The listener to call when the remote image is loaded
     * @param maxWidth The maximum width of the returned image.
     * @param maxHeight The maximum height of the returned image.
     * @return A container object that contains all of the properties of the request, as well as
     *     the currently available image (default if remote is not loaded).
     */
    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        // only fulfill requests that were initiated from the main thread.
        throwIfNotOnMainThread();

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

        // 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<?> newRequest =
            new ImageRequest(requestUrl, new Listener<Bitmap>() {
                @Override
                public void onResponse(Bitmap response) {
                    onGetImageSuccess(cacheKey, response);
                }
            }, maxWidth, maxHeight,
            Config.RGB_565, new ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    onGetImageError(cacheKey, error);
                }
            });

        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }
首先,检查当前是不是在主线程,接着检查当前请求的图片是不是在缓存中,如果是就直接返回,否则继续。缓存中不存在的话,先创建一个ImageContainer对象,这个时候就可以通知界面显示正式图片加载完成之前的默认图片了。在正式创建一个图片请求之前,再去检查一下这个请求是否已经存在,这个可以避免重复请求的发生。请求存在,则返回,不存在则正式创建一个图片请求对象——ImageRequest,最后返回。这个逻辑很简单。关键我们来看看这个ImageRequest的代码:
/**
 * A canned request for getting an image at a given URL and calling
 * back with a decoded Bitmap.
 */
public class ImageRequest extends Request<Bitmap> {
    /** Socket timeout in milliseconds for image requests */
    private static final int IMAGE_TIMEOUT_MS = 1000;

    /** Default number of retries for image requests */
    private static final int IMAGE_MAX_RETRIES = 2;

    /** Default backoff multiplier for image requests */
    private static final float IMAGE_BACKOFF_MULT = 2f;

    private final Response.Listener<Bitmap> mListener;
    private final Config mDecodeConfig;
    private final int mMaxWidth;
    private final int mMaxHeight;

    /** 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();

    /**
     * Creates a new image request, decoding to a maximum specified width and
     * height. If both width and height are zero, the image will be decoded to
     * its natural size. If one of the two is nonzero, that dimension will be
     * clamped and the other one will be set to preserve the image‘s aspect
     * ratio. If both width and height are nonzero, the image will be decoded to
     * be fit in the rectangle of dimensions width x height while keeping its
     * aspect ratio.
     *
     * @param url URL of the image
     * @param listener Listener to receive the decoded bitmap
     * @param maxWidth Maximum width to decode this bitmap to, or zero for none
     * @param maxHeight Maximum height to decode this bitmap to, or zero for
     *            none
     * @param decodeConfig Format to decode the bitmap to
     * @param errorListener Error listener, or null to ignore errors
     */
    public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
            Config decodeConfig, Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        setRetryPolicy(
                new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
    }

    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

    /**
     * Scales one side of a rectangle to fit aspect ratio.
     *
     * @param maxPrimary Maximum size of the primary dimension (i.e. width for
     *        max width), or zero to maintain aspect ratio with secondary
     *        dimension
     * @param maxSecondary Maximum size of the secondary dimension, or zero to
     *        maintain aspect ratio with primary dimension
     * @param actualPrimary Actual size of the primary dimension
     * @param actualSecondary Actual size of the secondary dimension
     */
    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
            int actualSecondary) {
        // If no dominant value at all, just return the actual.
        if (maxPrimary == 0 && maxSecondary == 0) {
            return actualPrimary;
        }

        // If primary is unspecified, scale primary to match secondary‘s scaling ratio.
        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 (resized * ratio > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }

    @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));
            }
        }
    }

    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     */
    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);
        } 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);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth);

            // 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));
        }
    }

    @Override
    protected void deliverResponse(Bitmap response) {
        mListener.onResponse(response);
    }

    /**
     * Returns the largest power-of-two divisor for use in downscaling a bitmap
     * that will not result in the scaling past the desired dimensions.
     *
     * @param actualWidth Actual width of the bitmap
     * @param actualHeight Actual height of the bitmap
     * @param desiredWidth Desired width of the bitmap
     * @param desiredHeight Desired height of the bitmap
     */
    // Visible for testing.
    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;
    }
}
如果认认真真的看完这个类,相信你对图片的加载会有一个比较深入的理解,所以建议认认真真的看这个类。
首先是构造方法,比我们之前介绍的Requst多了三个参数,int maxWidth,int maxHeight,Config decodeConfig,decodeConfig这个暂时我们可以不用去关心,就是告诉如何去解码bitmap,默认给的是Config.RGB_565,长宽这个字面意思不用解释,最大宽高,不传默认为实际大小。
接着就是protected Response<Bitmap> parseNetworkResponse(NetworkResponse response),关键代码在Response<Bitmap> doParse(NetworkResponse response)中,在这里我们可以看到,如果我们不设置最大宽高,就会直接解码图片,这个加载的就是实际大小的图片,占用内存就不太好控制了。如果我们设置了就会按照一定的规则去重新创建一个原始图片的缩略图,这个可以使加载到内存中的图片更好,避免OOM,具体怎么加载,为什么这样加载,大家可以参考附录中的介绍,也就是android 文档中的加载大图说明,看完之后,相信你会对大图的处理有非常深刻的理解。


4、下一篇

如果有机会,下一篇再写点关于HTTPS的东西,在此Mark一下,免得啥时候忘记咯


5、附录

参考:http://blog.csdn.net/guolin_blog/article/details/9526203 

google关于图片加载(这个对于处理图片加载很有帮助):

http://developer.android.com/training/displaying-bitmaps/index.html ;

http://blog.csdn.net/guolin_blog/article/details/9316683


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。