Android volley全局请求队列和图片加载

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_30276961/article/details/50118553

Android Volley网络通信详解一里,我介绍了Volley的基本特性,并且简单讲解了StringRequest和JsonObjectRequest。本篇,我们将创建一个全局的Volley请求,使你的应用方便使用volley。并且,讲解下volley的图片加载。

Volley Singleton

我们先来回顾一下,发出一个请求,volley需要做哪些:
1. 创建一个请求队列
2. 创建一个请求
3. 把请求添加到请求队列

从上述步骤来看,这个过程似乎也不是很麻烦。不过,如果你深入Volley的源码,你会发现,上述过程,如果针对要不断发出网络请求的应用,是不合理的。

可以看到,按照上述的步骤,我们每次发出请求,都要去创建一个请求队列,这明显是不靠谱的。从源码可以看到,按照上面的写法,的确是在不断创建请求队列。

    public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

        String userAgent = "volley/0";
        try {
            String packageName = context.getPackageName();
            PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
            userAgent = packageName + "/" + info.versionCode;
        } catch (NameNotFoundException e) {
        }

        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                stack = new HurlStack();
            } else {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);

        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            // No maximum size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            // Disk cache size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        queue.start();

        return queue;
    }

queue在每次newRequestQueue时会创建一个。

既然这种方案不合理,那怎么处理呢?合理的方案是,用一个单例去维护这个请求队列。

说到单例,在Android里,很容易想到Application。它在一个应用里只有一个。而且是全局的,哪里都能很容易拿到,可行。

不过,官网给我们提供的方案是自己维护一个。

public class VolleySingleton {
    private static VolleySingleton mInstance;
    private RequestQueue mRequestQueue;
    private ImageLoader mImageLoader;
    private static Context mContext;

    private VolleySingleton(Context context) {
        mContext = context;
        mRequestQueue = getRequestQueue();

        mImageLoader = new ImageLoader(
                mRequestQueue,
                new LruBitmapCache(context)
        );
    }

    public static synchronized VolleySingleton getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new VolleySingleton(context);
        }
        return mInstance;
    }

    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            // 如果你的应用时常使用volley,那么传入的context最好是application的context
            mRequestQueue = Volley.newRequestQueue(mContext.getApplicationContext());
        }
        return mRequestQueue;
    }

    public <T> void addToRequestQueue(Request req) {
        getRequestQueue().add(req);
    }

    public ImageLoader getImageLoader() {
        return mImageLoader;
    }
}

在Application或者自己写个,这两个方案都可取。
代码很简单,就是多了个ImageLoader。这个会在后面的图片加载里深入讲解。

创建了这个VolleySingleton,之前的StringRequest和JsonRequest就可以不用再创建一个请求队列了,直接通过
VolleySingleton.getInstance(this).addToRequestQueue(xxxRequest);就行。

图片加载

Volley提供了很强大的网络图片加载框架。之前我有写一个图片异步加载的照片墙应用,里面用到了Handler和Thread来模拟线程池,开了10个线程。Volley除了多线程执行外,还维护了一个本地缓存和内存缓存。所以在性能上更加高效。

扫描二维码关注公众号,回复: 3845092 查看本文章

ok,闲话少说。先来讲讲最基本的图片请求 : ImageRequest。

ImageRequest

ImageRequest和之前讲到的StringRequest用法一样,都是继承Request。所以,直接贴代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_request_test);
        mImg = (ImageView) findViewById(R.id.img);
        mImg.setImageResource(R.drawable.default_pic);

        final int screenWidth = DensityUtil.getScreenWidth(this);
        ImageRequest imageRequest = new ImageRequest(
                "http://pic31.nipic.com/20130711/9908282_130129471156_2.jpg",
                new Response.Listener<Bitmap>(){

                    @Override
                    public void onResponse(Bitmap response) {
                        mImg.setImageBitmap(response);
                    }
                }, screenWidth, screenWidth/2, ImageView.ScaleType.CENTER, Bitmap.Config.ARGB_8888,
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        mImg.setImageResource(R.drawable.default_pic);
                    }
                }
        );
        VolleySingleton.getInstance(this).addToRequestQueue(imageRequest);
    }

很简单吧,看看就行了。

ImageLoader

接着,我们来看下ImageLoader。
前面,有说到Volley的网络图片加载是很强大、高效的。我们开发一些应用,往往需要加载大量的图片,并且这些图片基本上都是放到listview或者gridview里。这里,就需要考虑到发出请求的取消和确认。因为,你在滑动中,是在不断刷新item的位置的,当你从某一页滑到另外一页时,原先的“过期”请求最好能取消,而当请求收到响应,还要再确认是否和当前的item的请求地址一致。

如果是用ImageRequest,那么,你就得像我之前写的那个照片墙应用一样,要自己维护上面这些情况。
有了Volley的ImageLoader,你就再也不用这么苦逼的去处理这些了。

查看源码,可以看到,ImageLoader没有继承Request,而是维护一个请求队列和图片缓存。

public class ImageLoader {
    /** RequestQueue for dispatching ImageRequests onto. */
    private final RequestQueue mRequestQueue;

    /** Amount of time to wait after first response arrives before delivering all responses. */
    private int mBatchResponseDelayMs = 100;

    /** The cache implementation to be used as an L1 cache before calling into volley. */
    private final ImageCache mCache;
    ......
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
        mRequestQueue = queue;
        mCache = imageCache;
    }
    ......
    public interface ImageCache {
        public Bitmap getBitmap(String url);
        public void putBitmap(String url, Bitmap bitmap);
    }

根据上述代码,初始化ImageLoader需要一个RequestQueue和一个ImageCache。前者我们已经能很轻松的获取到了。
来说说ImageCache这个接口。从这个接口可以看出,需要实现一个缓存,键值对是String和Bitmap。
我们可以考虑用本地缓存DiskLruCache,也可以考虑内存缓存(关于这块,可以参考我以前写的Android Bitmap大量使用不产生OOM之使用缓存机制)。
这里,我用内存缓存管理类–LruCache,在support 4里有。

public class LruBitmapCache extends LruCache<String, Bitmap>
        implements ImageLoader.ImageCache{

    public LruBitmapCache(int maxSize) {
        super(maxSize);
    }

    public LruBitmapCache(Context context) {
        this(getCacheSize(context));
    }

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

    private static int getCacheSize(Context context) {
        // 这里可以根据你应用的实际需要,定义合适大小
        // 这里设定缓存大小为3个整个界面所需图像数据大小
        final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        final int screenWidth = displayMetrics.widthPixels;
        final int screenHeight = displayMetrics.heightPixels;

        final int screenBytes = screenWidth * screenHeight * 4;
        return screenBytes * 3;
    }

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

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

可以看到,我创建了一个LruBitmapCache继承自LruCache,并实现了ImageCache接口。
这里,缓存大小设置为3个界面大小。ps : 一个像素点占据4字节。

缓存创建完之后,就是关联到ImageLoader上。

        mImageLoader = new ImageLoader(
                mRequestQueue,
                new LruBitmapCache(context)
        );

上述代码是在VolleySingleton里,在第一次初始化VolleySingleton时,就会创建一个ImageLoader。

ok,初始工作完毕,接着就是怎么和url,ImageView关联起来。

    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
    }

我贴了一个常用的加载方式,ScaleType它已经默认设置了。
可以看到,ImageLoader.get需要4个参数:
1. 图片url地址
2. 创建一个ImageListener
3. 图片最大宽度
4. 图片最大高度

我们先来看看第2个参数–ImageListener。

    public static ImageListener getImageListener(final ImageView view,
            final int defaultImageResId, final int errorImageResId)

上述代码是在ImageLoader里,也就是说,ImageListener可以从ImageLoader对象获取,需要传入3个参数:
1. 绑定图片到哪个ImageView
2. 没加载到图片时显示的图片资源
3. 加载结束,数据获取不到或数据异常时显示的加载出错图片

好了,ImageListener就介绍到这。接着讲讲下面两个参数:maxWidth和maxHeight。这两个参数设置,是为了告诉ImageLoader,我需要的图片最大只要这么宽,这么高,别超出,接近就行。然后在收到网络数据响应时,就会根据这两个值,对图片数据进行压缩。这样一来,最大限度的避免了OOM的发生。关于图片加载不出现OOM的方法可以参考: Android Bitmap大量使用不产生OOM之“加载大图片资源优化”
可以看下Volley的源码,它的处理方式和我上面提到的博客里的处理机制是一样的。ps : 最后是在ImageRequest的doParse。

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

需要注意的是,如果maxWidth和maxHeight都为0,那就不压缩了。上面代码可以看到。

ok, ImageLoader的初始化和使用都讲完了。接着就是上个实例:

这里写图片描述
可以看到,效率很高。

详细代码如下:

public class ImageLoadTest extends ActionBarActivity {
    GridView mGridView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_load_test);
        mGridView = (GridView) findViewById(R.id.gridview);
        GridAdapter mAdapter = new GridAdapter(this);
        mGridView.setAdapter(mAdapter);
    }

    private static class GridAdapter extends BaseAdapter {
        Context mContext;
        final int mSize;

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

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

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

        public GridAdapter(Context context) {
            mContext = context;
            // 屏幕的3分之一
            mSize = (DensityUtil.getScreenWidth(context)
                    - DensityUtil.dpToPx(1, context.getResources())) / 3;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = View.inflate(mContext, R.layout.gridview_image_item, null);
                viewHolder.imageView = (ImageView) convertView.findViewById(R.id.photo);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            AbsListView.LayoutParams params = (AbsListView.LayoutParams) viewHolder.imageView.getLayoutParams();
            if (params == null) {
                params = new AbsListView.LayoutParams(mSize, mSize);
                viewHolder.imageView.setLayoutParams(params);
            }
            load(viewHolder.imageView, position);
            return convertView;
        }

        private void load(ImageView imageView, int position) {
            ImageLoader.ImageListener imageListener
                    = ImageLoader.getImageListener(imageView,
                    R.drawable.default_pic, R.drawable.default_pic);
            VolleySingleton.getInstance(mContext)
                    .getImageLoader()
                    .get(ImageUrl.IMAGE_URL[position], imageListener, mSize, mSize);
        }
    }

    private static class ViewHolder {
        ImageView imageView;
    }

好了,接着讲解关于Volley图片加载的最后一个利器:NetworkImageView。
这个可以认为是ImageLoader的封装版,啥意思呢?就是把ImageLoader封装到ImageView里,如下;

public class NetworkImageView extends ImageView {
    /** The URL of the network image to load */
    private String mUrl;

    /**
     * Resource ID of the image to be used as a placeholder until the network image is loaded.
     */
    private int mDefaultImageId;

    /**
     * Resource ID of the image to be used if the network response fails.
     */
    private int mErrorImageId;

    /** Local copy of the ImageLoader. */
    private ImageLoader mImageLoader;
    ......

看到了吧,有个ImageLoader。那为啥要封装呢?其实就是为了方便我们更简单直观的写请求代码。举个例子,使用NetworkImageView发出请求是这样的:

imageView.setDefaultImageResId(R.drawable.default_pic);
imageView.setErrorImageResId(R.drawable.default_pic);
imageView.setImageUrl(ImageUrl.IMAGE_URL[position], VolleySingleton.getInstance(mContext).getImageLoader());

上面三句话,就是一个完整的请求,包含未加载和加载出错处理。怎么样,很直观简单吧。直接就是调用NetworkImageView.setImageUrl就发出请求了。这就是封装后的效果。

ok,效果和上面的ImageLoader一样,我就再贴下吧。
这里写图片描述
很快,很高效。

实例代码和ImageLoader的那个实例代码几乎差不多,唯一的区别就在于GridView的item从原来的ImageView替换成了NetworkImageView,还有一个是load()的不同。
关于NetworkImageView的load的代码上面已经贴出来了,就是那三句话 : setDefaultImageResId、setErrorImageResId、setImageUrl。

最后,还需要说明的是,NetworkImageView它也有图片压缩机制,它的图片压缩大小根据NetworkImageView可从其parent那里获得的大小决定。

好了,关于Volley的单例维护请求队列和ImageLoader;关于Volley各种图片加载请求方式;这两块都讲完了。(写得多了。。)

Good Day!

猜你喜欢

转载自blog.csdn.net/sinat_30276961/article/details/50118553