过完年,心情状态好了很多,终于可以静下心来写点东西。之前关于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
这个的缺点就是加载大量图片的时候可能更容易出现OOM问题,因为它没有处理图片的压缩,当然,你能保证你的图片很小或者你也可以重写NetworkImageView,给它添加一个设置加载尺寸的方法。
3、分析:ImageLoader和ImageRequest
/** * 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; } }如果认认真真的看完这个类,相信你对图片的加载会有一个比较深入的理解,所以建议认认真真的看这个类。
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