这篇文章是系列文章An Introduction to Volley的一部分,下一篇文章是:Creating a Weather Application for Mars Using Volley
Volley是谷歌2013年在I/O大会期间推出的网络库。开发Volley是因为在Android SDK中缺乏一个用户体验良好的网络加载类。
在Volley发布之前,开发具有客户端与服务端交互程序的唯一工具是标准的java类java.net.HttpURLConnection以及Apache 的 org.apache.http.client。
除开这两个工具的bug不说,所有http链接之外的事情都必须完全自己写,如果你想缓存图片或者发送一个请求,你必须从零开始开发。
幸运的是,我们现在有了Volley,完全填补了我们的这些需求。
在较低的api版本中(多数是在Gingerbread和Froyo中),HttpUrlConnection和HttpClient 远未达到完美。有一些已知的问题 和bugs 一直未被修复,HttpClient 自上次api更新(API 22)之后就已经过时,意味着不再维护,后续可能也会被移除。
这就是需要转到一个更稳定的处理网络请求的方法的主要原因。
自从引入Honeycomb(API 11)以来,系统强制网络操作必须运行在单独的线程中,UI线程之外。这个重大的变化导致AsyncTask
要使用AsyncTask,首先你得在onPreExecute做一些准备工作,比如定义context。然后在doInBackground 方法中执行后台任务,最后在onPostExecute中处理结果。这要比实现service更简单直接,因此有大量的例子和文章出现。
但是AsyncTask的主要问题是调用的顺序。你无法决定哪个请求走在最前面,哪个请求必须等待,所有的请求都是按照先进先出的顺序,FIFO。
在某些情况下,问题就出来了,比如当你需要加载一个item中带有图片的列表。当用户向下滚动想获得新的数据时,你无法告诉Activity先加载下一页的json数据,只有慢慢等待上一页的图片加载完。对于像facebook和Twiitter这种item数据的重要性远大于相关图片的应用来说,这会导致严重的用户体验问题。
Volley用其强大的cancellation api解决了这个问题。当调用的时候,你不再需要在onPostExecute中检查Activity是否被销毁。这帮助我们避免了不想要的NullPointerException。
前些时候,Google+团队针对每种可以使用的网络请求方式做了一些列的性能测试。当应用在RESTful 风格的应用中时,Volley的得分比其他候选方法高近10倍。
Volley可以自动缓存请求,这点确实是关乎生死的问题。让我们暂时回到上面所提到的那个例子。你有一个item的列表 - 假设是JSON 数组 - 并且每个item都包括一段描述和一个缩略图。现在设想一下用户旋转屏幕之后会发生的事情:activity销毁,列表重新加载,同样的还有图片。长话短说就是:严重的资源浪费以及糟糕的用户体验。
事实证明,Volley在克服这种困难中是相当有用的。它可以记住前一个调用同时处理Activity的销毁与重建。它缓存所有的东西,这点你也不用担心。
Volley在轻量级的调用中堪称完美,比如请求JSON对象,或者列表的一部分,或者被选中item的详情等等。它是为RESTful应用设计的,在这种特定场景下可以发挥自己最好的优势。
但是当把它应用在数据流或者大文件下载中,就不是那么好了。这就是为什么它叫Volley(万箭齐发),而不是叫加农炮。
Volley分为三层,每一层都工作在自己的线程中。
在主线程中,你只允许触发请求与处理返回结果,不能多也不能少。
其结果就是你实际上可以忽略在使用AsyncTask的时候doInBackground 方法里面所做的事情。Volley 自动管理http传输同时捕获网络错误,这些都是以前需要我们自己考虑的。
当你向队列中添加了一个请求,背后发生了几件事情。Volley会检查这个请求是否可以从缓存中得到。如果可以,缓存将负责读取,解析,和分发。否则将传递给网络线程。
在网络线程中,一些列的轮询线程不断的在工作。第一个可用的网络线程线程让请求队列出列,发出http请求,解析返回的结果,并写入到缓存中。最后,把解析的结果分发给主线程的listener中。
Volley设置起来并不是很方便,貌似没有官方的Maven repository,这让人理解不能。你必须依赖官方的源代码。
第一件事就是,从它的repository 下载Volley源码。这个很简单,下面的Git命令可以为你做完所有的工作:
1
|
git clone https:
//android.googlesource.com/platform/frameworks/volley
|
几周之前,你还可以使用ant命令行(android update project -p . 然后 ant jar)将所有的东西都打包成jar,然后直接在Android Studio项目中使用compile files('libs/volley.jar')来导入jar。
但是最近,google更新了Volley成Android Studio的构建风格,让它很难创建一个标准的jar,你仍然可以这样做,但是只能使用老版本的库,虽然这种方式也许是最快速的的,但个人不推荐你使用这种方式。
你应该使用经典的方式来设置Volley,将源码作为一个module导入。在Android Studio中,在打开项目的情况下,选择File > New Module,然后选择Import Existing Project。选择你下载的源码的所在目录然后确认。一个名为Volley的文件夹将出现在你的项目结构中。Android Studio会自动的更新settings.gradle文件以包含Volley module,因此你只需添加你的依赖compile project(':volley'),然后就完成了。
不过还有第三种方法,你可以在build.gradle 文件的依赖部分添加这行代码:
1
|
compile
'com.mcxiaoke.volley:library-aar:1.0.15'
|
这是谷歌官方repository的一个镜像,通常会同步更新。这可能是最简单快速的方法,但是记住,这不是官方的Maven repository,没有保证,不被谷歌支持。
在我看来,最好还是多花几分钟导入官方的代码比较好。这样你就可以轻松的跳到原始定义与实现的代码中,如果需要,你还可以修改Volley。
通常Volley只会用到两个类RequestQueue 和Request,你首先创建一个RequestQueue,RequestQueue管理工作线程并将解析的结果发送给主线程。然后你传递一个或者多个Request
对象给他。
Request 的构造函数的参数总是包含类型(GET, POST, 等等),数据源的url,以及事件监听者。根据请求类型的不同,可能还需要一些其他的参数。
在接下来的例子中,我通过Volley.newRequestQueue方法创建了一个RequestQueue 对象,使用Volley定义的默认值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
String url =
"http://httpbin.org/html"
;
// Request a string response
StringRequest stringRequest =
new
StringRequest(Request.Method.GET, url,
new
Response.Listener
@Override
public void onResponse(String response) {
// Result handling
System.out.println(response.substring(0,100));
}
},
new
Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// Error handling
System.out.println(
"Something went wrong!"
);
error.printStackTrace();
}
});
// Add the request to the queue
Volley.newRequestQueue(
this
).add(stringRequest);
|
正如你看到的那样,这非常直接:创建一个请求,然后将它添加到请求队列,完成。
注意listener的语法类似AsyncTask.onPostExecute,只是变成了onResponse。这并不是巧合。Volley 的开发者故意将库的api做的和AsyncTask 方法类似。这样从AsyncTask转到Volley就会轻松很多。
如果你需要在几个Activity中触发多个请求,你应该避免使用上面的方法Volley.newRequestQueue.add。在整个项目中实例化一个共享的请求队列会更好些。:
MySingletonClass.getInstance().getRequestQueue().add(myRequest);
我们这个系列的下一篇教程中我们将看到这种用法。
Volley实现了三种常见的请求类型:
StringRequest
ImageRequest
JsonRequest
每个类都是继承自前面使用了的Request类。我们已经在上个例子中使用了StringRequest ,下面让我们来看看JsonRequest是如何工作的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
String url =
"http://httpbin.org/get?site=code&network=tutsplus"
;
JsonObjectRequest jsonRequest =
new
JsonObjectRequest
(Request.Method.GET, url,
null
,
new
Response.Listener
@Override
public void onResponse(JSONObject response) {
// the response is already constructed as a JSONObject!
try
{
response = response.getJSONObject(
"args"
);
String site = response.getString(
"site"
),
network = response.getString(
"network"
);
System.out.println(
"Site: "
+site+
"\nNetwork: "
+network);
}
catch
(JSONException e) {
e.printStackTrace();
}
}
},
new
Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
error.printStackTrace();
}
});
Volley.newRequestQueue(
this
).add(jsonRequest);
|
很漂亮是吧?你可以看到,返回结果的类型已经是JSONObject的了。如果你需要,你还可以请求JSONArray ,只需用JsonArrayRequest 替代aJsonObjectRequest就可以了。
跟之前一样,构造函数的第一个参数是使用的http方法,然后提供获取json的url参数,第三个参数是null,表示请求url不需要带其他请求参数。最后是用于接收返回JSON的listener以及错误listener。如果你想忽略错误,可以为null。
获取图片则需要更多的工作。有三种请求图片的方法。ImageRequest 是标准方法。通过提供的Url,她将你请求的图片显示在一个普通的ImageView中。压缩与大小调整的操作都发生在工作线程中。第二种选择是ImageLoader
类。你可以将之想象成数量庞大的ImageRequests,比如生成一个带有图片的ListView。第三种选择是NetworkImageView,
下面来看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
String url =
"http://i.imgur.com/Nwk25LA.jpg"
;
mImageView = (ImageView) findViewById(R.id.image);
ImageRequest imgRequest =
new
ImageRequest(url,
new
Response.Listener
@Override
public void onResponse(Bitmap response) {
mImageView.setImageBitmap(response);
}
}, 0, 0, ImageView.ScaleType.FIT_XY, Bitmap.Config.ARGB_8888,
new
Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
mImageView.setBackgroundColor(Color.parseColor(
"#ff0000"
));
error.printStackTrace();
}
});
Volley.newRequestQueue(
this
).add(imgRequest);
|
第一个参数是图片的url,第二个是结果的listener,第三、第四个参数是maxWidth(最大宽度) 和 maxHeight(最大高度),你可以设置为0来忽略他们。然后是用于计算图片所需大小的ScaleType,然后是用于指定图片压缩方式的参数,我建议总是使用 Bitmap.Config.ARGB_8888,最后是一个错误listener。
注意Volley默认会将这种请求的优先级设置为low。
1
2
3
4
5
6
|
// Snippet taken from ImageRequest.java,
// in the Volley source code
@Override
public Priority getPriority() {
return
Priority.LOW;
}
|
注:因为是请求图片,所以没有http类型参数,图片请求总是get的。
从get请求切换到post请求是非常简单的。你只需要在request的构造方法中改变Request.Method,同时重写getParams方法,返回包含请求参数的Map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
String url =
"http://httpbin.org/post"
;
StringRequest postRequest =
new
StringRequest(Request.Method.POST, url,
new
Response.Listener
@Override
public void onResponse(String response) {
try
{
JSONObject jsonResponse =
new
JSONObject(response).getJSONObject(
"form"
);
String site = jsonResponse.getString(
"site"
),
network = jsonResponse.getString(
"network"
);
System.out.println(
"Site: "
+site+
"\nNetwork: "
+network);
}
catch
(JSONException e) {
e.printStackTrace();
}
}
},
new
Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
error.printStackTrace();
}
}
) {
@Override
protected Map
{
Map
new
HashMap<>();
// the POST parameters:
params.put(
"site"
,
"code"
);
params.put(
"network"
,
"tutsplus"
);
return
params;
}
};
Volley.newRequestQueue(
this
).add(postRequest);
|
如果你想取消所有的请求,在onStop方法中添加如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
@Override
protected void onStop() {
super
.onStop();
mRequestQueue.cancelAll(
new
RequestQueue.RequestFilter() {
@Override
public boolean apply(Request> request) {
// do I have to cancel this?
return
true
;
// -> always yes
}
});
}
|
这样你就不必担心在onResponse被调用的时候用户已经销毁Activity。这种情况下会抛出NullPointerException异。但是post请求则需要继续,即使用户已经改变了Activity。我们可以通过使用tag来做到,在构造GET请求的时候,添加一个tag给它。
1
2
3
|
// after declaring your request
request.setTag(
"GET"
);
mRequestQueue.add(request);
|
如果要取消GET请求,只需简单的添加下面的一行代码:
1
|
mRequestQueue.cancelAll(
"GET"
);
|
这样你就只会取消GET请求,让其它请求不受影响。注意你必须手动在销毁的Activity中处理这种情况。
Volley doesn't provide a method for setting the cookies of a request, nor its priority. It probably will in the future, since it's a serious omission. For the time being, however, you have to extend the Request class.
Volley并没有提供设置一个请求的cookies以及优先级的方法。也许在将来会有,毕竟这是一个很严重的疏忽。但是目前,你需要继承Request类。
对于cookies的管理,你需要添加请求的header,重写getHeaders方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class CustomRequest extends JsonObjectRequest {
// Since we're extending a Request class
// we just use its constructor
public CustomRequest(int method, String url, JSONObject jsonRequest,
Response.Listener
super
(method, url, jsonRequest, listener, errorListener);
}
private Map
new
HashMap<>();
/**
* Custom class!
*/
public void setCookies(List
StringBuilder sb =
new
StringBuilder();
for
(String cookie : cookies) {
sb.append(cookie).append(
"; "
);
}
headers.put(
"Cookie"
, sb.toString());
}
@Override
public Map
return
headers;
}
}
|
有了上面的代码,你就可以直接使用setCookies方法为请求提供cookie列表了。
1
2
3
4
5
6
7
8
9
10
11
12
|
// Firstly, you create the list of the cookies,
// conformed to the HTTP conventions
// i.e. key=value
List
new
ArrayList<>();
cookies.add(
"site=code"
);
cookies.add(
"network=tutsplus"
);
// then you invoke your custom method
customRequest.setCookies(cookies);
// and finally add the request to the queue
Volley.newRequestQueue(
this
).add(customRequest);
|
而对于优先级,你同样需要继承Request类,重写getPriority方法。下面是代码的样子:
1
2
3
4
5
6
7
8
9
10
11
12
|
Priority mPriority;
public void setPriority(Priority priority) {
mPriority = priority;
}
@Override
public Priority getPriority() {
// If you didn't use the setPriority method,
// the priority is automatically set to NORMAL
return
mPriority !=
null
? mPriority : Priority.NORMAL;
}
|
然后,在主线程中,调用下面的一行代码来设置请求的优先级:
1
|
customRequest.setPriority(Priority.HIGH);
|
你可以从如下的优先级中选择一个:
1
2
3
4
|
Priority.LOW
// images, thumbnails, ...
Priority.NORMAL
// residual
Priority.HIGH
// descriptions, lists, ...
Priority.IMMEDIATE
// login, logout, ...
|
在这篇文章中,我们看到了Volley是如何工作的。我们首先看到了为什么使用Volley而不是Android SDK中自带的其他解决方案。然后我们深入到库的内部,查看它的工作流程以及支持的请求类型。最后,我们动手创建了几个简单的Request,并实现了一个可以设置优先级与cookies的自定义Request。
在下一部分中,我们将创建一个使用了Volley的简单应用。向你演示如何使用探索者号在火星上收集的天气数据来制作一个火星天气app。