本篇博客主要记录一下Volley的基本用法,为之后分析Volley源码做一下铺垫。
在分析Volley的具体用法前,我们先来简单了解一下Volley:
Volley是Android平台上的网络通信库,能使网络通信更快,更简单,更健壮。
Volley非常适合进行数据量不大,但通信频繁的网络操作,包括图片下载等。
对于大数据量的网络操作,比如说下载文件等,Volley的表现比较糟糕。
在Android源码中,Volley相关的代码被放置在frameworks/volley目录下。
我们还是以一个Demo的形式,来看看Volly的用法,demo的主界面如下图所示:
如图所示,界面的上方有一排按键,对应着使用Volley访问网络时,常用的一些功能。
界面左下方是用ScrollView包裹的TextView,以字符的形式显示网络访问的结果;
界面的右下方,将以图片的形式显示网络访问的结果。
界面布局及初始化组件的代码,就不在博客中记录了,我们主要看一下不同按键被点击后的操作。
在使用Volley前,应用需要配置网络访问的权限:
<uses-permission android:name="android.permission.INTERNET"/>
然后在应用启动时,创建Volley的RequestQueue,我的demo在onCreate函数中创建的:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//使用Volley的静态方法即可,传入Context
//这里我用的是ApplicationContext,因为我还没仔细看过Volley的源码,
//所以给它ApplicationContext最稳妥,避免内存泄露
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
findChildView();
configChildView();
}
按照网上的说法,Volley的RequestQueue是一个请求队列,
它可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求。
因此我们不必为每一次HTTP请求都创建一个RequestQueue。
有了RequestQueue后,我们就可构造网络请求Request了。
这就如同Handler的Looper运行后,就可以往里放Message了。
我们首先看看第一个按键被点击后的操作,
这个按键主要用于向网络发送一个StringRequest,
对应代码如下:
.............
private void makeStringRequest() {
//构造StringRequest
StringRequest stringRequest = new StringRequest(
//指定Url
"https://www.baidu.com/",
//成功回调接口
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
mTextView.setText(response);
}
},
//失败回调接口
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
mTextView.setText(error.toString());
}
});
//将StringRequest加入到RequestQueue中
mRequestQueue.add(stringRequest);
}
.............
StringRequest的含义其实就是,网络返回的结果为String。
我们看看源码中StringRequest的构造函数:
/**
* Creates a new GET request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
/**
* Creates a new request with the given method.
*
* @param method the request {@link Method} to use
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
从StringRequest中源码的注释可以看出,
StringRequest可以携带网络对应的地址、访问成功和失败的回调接口,
同时可以指定本次请求对应的Http Method Type。
在demo中,我们发送的是一个Http Get请求,即试图从网络中得到信息;
同样,我们可以发送一个Http Post请求,只要指定StringRequest的method为Method.Post即可。
不过StringRequest并不能直接携带Post参数,如果需要只能手动创建一个匿名类,
并重写其父类的getParams方法,代码类似于:
StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener) {
@Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String, String> map = new HashMap<String, String>();
map.put("params1", "value1");
map.put("params2", "value2");
return map;
}
};
源码中关于getParams的注释如下:
/**
* Returns a Map of parameters to be used for a POST or PUT request. Can throw
* {@link AuthFailureError} as authentication may be required to provide these values.
*
* <p>Note that you can directly override {@link #getBody()} for custom data.</p>
*
* @throws AuthFailureError in the event of auth failure
*/
protected Map<String, String> getParams() throws AuthFailureError {
return null;
}
打开网络的前提下,StringRequest的demo运行效果如下:
ScrollView可以滑动显示全部信息。
点击第二个按键,demo将向网络发送一个JSONRequest。
其基本操作与StringRequest类似,差别仅是要求网络以JSONObject的格式返回结果。
对应代码如下:
private void makeJsonRequest() {
//构造一个url,我这里用的是百度图片的url
String url = Uri.parse("http://image.baidu.com/search/index?")
.buildUpon()
.appendQueryParameter("tn", "resultjson")
.appendQueryParameter("word", "微距摄影")
.build().toString();
//构造一个JSONObjectRequest
JsonObjectRequest objectRequest = new JsonObjectRequest(
//多了第二个参数,这里可以传入一个JSONObject
//为null时,表示我们发送的是Http Get
//若不为null,表示发送的是Http Post
//也可以显示指定Http Method Type
url, null,
new Response.Listener<JSONObject>(){
@Override
public void onResponse(JSONObject response) {
mTextView.setText(response.toString());
}
},
new Response.ErrorListener(){
@Override
public void onErrorResponse(VolleyError error) {
mTextView.setText(error.toString());
}
});
//加入队列
mRequestQueue.add(objectRequest);
}
demo发送JSONRequest后的效果如下图:
按照同样的写法,还可以使用JSONArrayRequest,此处就不做赘述了。
点击第三个按键,demo将向网络发送一个Image Request,用于下载图片。
对应代码如下:
private void makeImageRequest() {
//随便找了张百度图片,看看网页的Html代码,就可盗用其url
String url = "http://img3.imgtn.bdimg.com/it/u=1382485185,1958087964&fm=23&gp=0.jpg";
ImageRequest imageRequest = new ImageRequest(url,
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
//设置下载得到的图片
mImageView.setImageBitmap(response);
}
//与之前的差别主要在这里
//分别指定下载图片的最大宽度、最大高度及颜色属性
//如果指定的网络图片的宽度或高度大于这里的最大值,则会对图片进行压缩
//指定成0的话就表示不管图片有多大,都不会进行压缩
}, 200, 200, Bitmap.Config.ARGB_8888,
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
//设置一个失败后的图片
//demo偷懒了一下,直接放了个启动图片
mImageView.setImageResource(R.mipmap.ic_launcher);
}
}
);
mRequestQueue.add(imageRequest);
}
可以看到除了多了几个修饰图片的参数外,ImageRequest与之前的用法也基本一致。
demo发送ImageRequest后的效果如下:
接下来我们看看Volley中ImageLoader的用法:
..................
private LocalImageCache mLocalImageCache = new LocalImageCache();
private ImageLoader mImageLoader;
private void loadImage() {
if (mImageLoader == null) {
//ImageLoader需要和RequestQueue、ImageCache绑定
mImageLoader = new ImageLoader(mRequestQueue, mLocalImageCache);
}
//与前面的Request一样,ImageLoader也需要定义对应的回调接口
//Volley中的ImageLoader提供了创建回调接口的方法
//可以依次指定图片下载后需要放置的位置,默认图片(下载时间较长时,临时显示的背景图)及下载失败的图片
ImageLoader.ImageListener imageListener = ImageLoader
.getImageListener(mImageView, R.mipmap.ic_launcher, R.mipmap.ic_launcher);
String url = "http://img1.imgtn.bdimg.com/it/u=1245538184,752165177&fm=23&gp=0.jpg";
//最后,利用ImageLoader的get方法获取图片
//从这里可以看出,ImageLoader也可以指定下载图片的宽、高
mImageLoader.get(url, imageListener, 200, 200);
}
//这里的ImageCache实际上就是一个存储Bitmap的LruCache
private class LocalImageCache implements ImageLoader.ImageCache {
private LruCache<String, Bitmap> mCache;
LocalImageCache() {
int maxSize = 10 * 1024 * 1024;
mCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return mCache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
mCache.put(url, bitmap);
}
}
..............
从上面的代码不难看出,ImageLoader与ImageRequest相比,最大的优势就是引入了Cache。
这样当终端请求同样网址的信息时,会优先从Cache中取出结果;
Cache中没有对应结果时,才会向网络发起请求。
demo利用ImageLoader下载图片的效果如下:
(换了张图片,显示在同样的位置)
接下来,我们看看Volley中定义的控件NetworkImageView的用法。
先来看看demo中最初使用的代码:
..............
private void refreshNetworkImageView() {
//在我的布局文件中,并没有直接使用NetworkImageView
//仅仅用一个FrameLayout作为Container
if (mNetworkImageViewContainer.getChildCount() != 0) {
mNetworkImageViewContainer.removeAllViewsInLayout();
}
//动态创建NetworkImageView
NetworkImageView networkImageView = new NetworkImageView(this);
networkImageView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
if (mImageLoader == null) {
mImageLoader = new ImageLoader(mRequestQueue, mLocalImageCache);
}
//NetworkImageView也能设置默认图片和错误图片
networkImageView.setDefaultImageResId(R.mipmap.ic_launcher);
networkImageView.setErrorImageResId(R.mipmap.ic_launcher);
//然后设置url和ImageLoader,不难看出,NetworkImageView的实际下载工作,也将交给ImageLoader来完成
networkImageView.setImageUrl("http://img1.imgtn.bdimg.com/it/u=494242567,3557760312&fm=11&gp=0.jpg",
mImageLoader);
//显示NetworkImageView
mNetworkImageViewContainer.addView(networkImageView);
}
....................
按照上述代码,每次点击对应按键时,都会重新创建NetworkImageView,然后进行图片下载工作。
这里我为什么选择动态创建NetworkImageView呢?
因为最初时我没有找到更合适的办法。
我自己测试了一下,按照上述代码,当在布局中直接使用NetworkImageView时,
上述代码修改为:
........
if (mImageLoader == null) {
mImageLoader = new ImageLoader(mRequestQueue, mLocalImageCache);
}
//直接从布局中取得networkImageView,不再动态创建了
networkImageView.setDefaultImageResId(R.mipmap.ic_launcher);
networkImageView.setErrorImageResId(R.mipmap.ic_launcher);
networkImageView.setImageUrl("http://img1.imgtn.bdimg.com/it/u=494242567,3557760312&fm=11&gp=0.jpg",
mImageLoader);
...........
在这种情况下,当网络处于断开时,点击NetworkImageView对应的按键,NetworkImageView将显示错误图片。
然后连接网络,再次点击对应的按键,发现NetworkImageView并不会进行下载操作。
为了弄懂这个问题,自己看了看NetworkImageView的源码:
..................
//控件被绘制时,会调用loadImageIfNecessary,参数为true
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
loadImageIfNecessary(true);
}
..................
//给控件设置url和imageLoader时,也会调用loadImageIfNecessary,参数为false
public void setImageUrl(String url, ImageLoader imageLoader) {
setImageUrl(url, imageLoader, null);
}
public void setImageUrl(String url, ImageLoader imageLoader, ImageURLBuilder urlBuilder) {
mUrl = url;
mImageLoader = imageLoader;
mUrlBuilder = urlBuilder;
// The URL has potentially changed. See if we need to load it.
loadImageIfNecessary(false);
}
...................
从这部分代码,可以看出当NetworkImageView被绘制后,或调用setImageUrl时,均会调用loadImageIfNecessary方法。
我们跟进该方法:
void loadImageIfNecessary(final boolean isInLayoutPass) {
//图像宽高处理
..........
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
// 这里很重要!,后问分析
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
...........
// rebuild url if needed
String tmpUrl = mUrl;
if (mUrlBuilder != null) {
tmpUrl = mUrlBuilder.buildUrl(
mUrl, maxWidth, maxHeight, getScaleType(), getContext().getApplicationContext());
}
// if there was an old request in this view, check if it needs to be canceled.
// 问题就出在这里,后文分析
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(tmpUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
// The pre-existing content of this view didn't match the current URL. Load the new image
// from the network.
// 进行下载工作,并创建ImageContainer
ImageContainer newContainer = mImageLoader.get(tmpUrl,
new ImageListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (mErrorImageId != 0) {
setImageResource(mErrorImageId);
}
}
@Override
public void onResponse(final ImageContainer response, boolean isImmediate) {
// If this was an immediate response that was delivered inside of a layout
// pass do not set the image immediately as it will trigger a requestLayout
// inside of a layout. Instead, defer setting the image by posting back to
// the main thread.
// 这里可以看出,输入参数仅辅助决定是否立即显示回复信息
if (isImmediate && isInLayoutPass) {
post(new Runnable() {
@Override
public void run() {
onResponse(response, false);
}
});
return;
}
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
}
}, maxWidth, maxHeight, scaleType, mTransformation);
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}
根据上面的代码,我们看一下直接在布局中使用NetworkImageView时的流程:
1、布局被绘制时,我们并没有设置url及ImageLoader,
因此当onLayout被调用时,尽管loadImageIfNecessary被调用,但在上述代码的这个位置将阻止下载工作:
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
//url为null,因此加载的是default或null image
setDefaultImageOrNull();
return;
}
2、首次为NetworkImageView设置url和imageLoader,仍会调用loadImageIfNecessary方法。
这次在loadImageIfNecessary中,将利用mImageLoader创建出mImageContainer,并进行下载操作。
然而一旦网络不可用,将调用setImageResource(mErrorImageId)设置错误图片,详见上文中的代码。
3、当网络可用时,我们再次为NetworkImageView设置同样的url和imageLoader,
触发loadImageIfNecessary方法,此时这部分代码将生效:
// if there was an old request in this view, check if it needs to be canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
//由于之前已经创建过mImageContainer,且使用的是相同的url,因此此处直接返回
//不再进行后面的下载操作了
if (mImageContainer.getRequestUrl().equals(tmpUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different URL.
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
因此,当网络发生变化时,对于同一个url,必须清除旧有的mImageContainer,才能进行下载工作。
因此demo代码中,最初的设计是每次点击按键,都重新加载新的NetworkImageView。
不过这么做着实麻烦,我们再来回头看看loadImageIfNecessary的代码:
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
// 只要每次点击按键时,先传入一个empty url不就可以清除旧有的ImageContainer么!!!
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
最终修改demo如下:
private void refreshNetworkImageView() {
if (mImageLoader == null) {
mImageLoader = new ImageLoader(mRequestQueue, mLocalImageCache);
}
mNetworkImageView.setDefaultImageResId(R.mipmap.ic_launcher);
mNetworkImageView.setErrorImageResId(R.mipmap.ic_launcher);
//相当于每次点击时,强制刷新一下
mNetworkImageView.setImageUrl("", mImageLoader);
mNetworkImageView.setImageUrl("http://img1.imgtn.bdimg.com/it/u=494242567,3557760312&fm=11&gp=0.jpg",
mImageLoader);
}
按照这种方式,当网络从断开变为连接时,重新点击对应按键,可以加载正确的图片。
整个demo运行效果如下:
最后提一下:
NetworkImageView并不需要提供任何设置最大宽、高的方法也能够对加载的图片进行压缩。
这是由于NetworkImageView是一个控件,在加载图片的时候它会自动获取自身的宽高,
然后对比网络图片的宽度,决定是否需要对图片进行压缩,即压缩过程是在内部完全自动化的。
如果你不想对图片进行压缩的话,只需要在布局文件中,
把NetworkImageView的layout_width和layout_height都设置成wrap_content就可以了。
这样NetworkImageView就会将该图片的原始大小展示出来,不会进行任何压缩。
以上就是Volley中比较基本的用法,接下来看看如何利用Volley框架定制自己的Request。
我们首先看看Volley原生的StringRequest是如何实现的:
/**
* A canned request for retrieving the response body at a given URL as a String.
*/
//继承模板类,模板参数表示返回结果
public class StringRequest extends Request<String> {
private final Listener<String> mListener;
/**
* Creates a new request with the given method.
*
* @param method the request {@link Method} to use
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
//定义构造函数,注意调用父类即可
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
/**
* Creates a new GET request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
//需要覆盖的函数,将结果递交给listener
@Override
protected void deliverResponse(String response) {
mListener.onResponse(response);
}
//需要覆盖的函数,解析网络返回的结果
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
}
有了StringRequest的源码后,我们依葫芦画瓢即可写出自定义的Request。
demo中定义了XMLRequest,用于获取网络的xml文件:
//基本定义与StringRequest一致,只是将模板参数替换为XmlPullParser
public class XMLRequest extends Request<XmlPullParser> {
private final Response.Listener<XmlPullParser> mListener;
public XMLRequest(int method, String url, Response.Listener<XmlPullParser> listener,
Response.ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
public XMLRequest(String url, Response.Listener<XmlPullParser> listener,
Response.ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
@Override
protected Response<XmlPullParser> parseNetworkResponse(NetworkResponse response) {
try {
//这里指定以UTF-8格式,解析返回的结果
String xmlString = new String(response.data,
"UTF-8");
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xmlPullParser = factory.newPullParser();
//用XmlPullParser封装网络返回的字符
xmlPullParser.setInput(new StringReader(xmlString));
//然后返回结果
return Response.success(xmlPullParser, HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (XmlPullParserException e) {
return Response.error(new ParseError(e));
}
}
@Override
protected void deliverResponse(XmlPullParser response) {
mListener.onResponse(response);
}
}
我们看看XMLRequest的使用:
//点击最后一个按键,触发该该函数
private void makeXmlRequest() {
XMLRequest xmlRequest = new XMLRequest(
//设置url
"http://flash.weather.com.cn/wmaps/xml/china.xml",
new Response.Listener<XmlPullParser>() {
@Override
public void onResponse(XmlPullParser response) {
//我是在另一个界面显示结果的
//getWeatherInfo函数,负责从XmlPullParser中获取数据
Intent intent = CustomActivity.getIntent(
getApplicationContext(), getWeatherInfo(response));
getApplicationContext().startActivity(intent);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(getApplicationContext(), error.toString(),
Toast.LENGTH_LONG).show();
}
});
//将Request加入到RequestQueue中
mRequestQueue.add(xmlRequest);
}
“http://flash.weather.com.cn/wmaps/xml/china.xml“对应网址的xml文件格式如下:
根据xml文件格式,对应的解析函数如下:
private ArrayList<WeatherModel> getWeatherInfo(XmlPullParser xmlPullParser) {
ArrayList<WeatherModel> rst = new ArrayList<>();
WeatherModel weatherModel;
try {
int eventType = xmlPullParser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
//找到city对应的元素
if (xmlPullParser.getName().equals("city")) {
weatherModel = new WeatherModel();
//读取我们需要的属性,并保存
weatherModel.mProvinceName = xmlPullParser.getAttributeValue(0);
weatherModel.mCityName = xmlPullParser.getAttributeValue(2);
weatherModel.mStateDetailed = xmlPullParser.getAttributeValue(5);
weatherModel.mTemperature = xmlPullParser.getAttributeValue(7)
+ "~" + xmlPullParser.getAttributeValue(6);
weatherModel.mWindState = xmlPullParser.getAttributeValue(8);
rst.add(weatherModel);
}
break;
}
eventType = xmlPullParser.next();
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return rst;
}
解析完成后,就可以在另一个界面显示了。
此处WeatherModel为我定义的保存信息的模型:
//为了便于bundle传输信息,继承了Parcelable接口
public class WeatherModel implements Parcelable{
String mProvinceName;
String mCityName;
String mStateDetailed;
String mTemperature;
String mWindState;
public WeatherModel() {
}
public WeatherModel(Parcel in) {
mProvinceName = in.readString();
mCityName = in.readString();
mStateDetailed = in.readString();
mTemperature = in.readString();
mWindState = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mProvinceName);
dest.writeString(mCityName);
dest.writeString(mStateDetailed);
dest.writeString(mTemperature);
dest.writeString(mWindState);
}
public static final Parcelable.Creator<WeatherModel> CREATOR =
new Parcelable.Creator<WeatherModel> () {
@Override
public WeatherModel createFromParcel(Parcel source) {
return new WeatherModel(source);
}
@Override
public WeatherModel[] newArray(int size) {
return new WeatherModel[size];
}
};
@Override
public String toString() {
return mProvinceName + ", " + mCityName + ", "
+ mStateDetailed + ", " + mTemperature + ", " + mWindState;
}
}
最后,demo显示的效果如下:
至此,Volley框架的基本用法记录完毕,之后再来分析一下Volley框架整体的源码。
本文涉及demo地址如下:
https://github.com/ZhangJianIsAStark/Demos/tree/master/volleytest