您的位置:首页 > 移动开发 > Android开发

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

2015-12-01 04:52 519 查看
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除了多线程执行外,还维护了一个本地缓存和内存缓存。所以在性能上更加高效。

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!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息