前言
寒假学习了一下安卓的网络通信部分,扩展和封装了volley,还是挺有意思的,所以写一篇博客来记录一下整个历程吧。大家都知道,安卓网络通信有很多解决方案,比如HttpURLConnection,OkHttp,Android-async-http,Volley等,那为什么是Volley+OkHttp3+Gson(Jackson)?答案是这样的,用volley来进行网络通信,用Okhttp3来处理Volley的底层HTTP请求,然后用Gson或者Jackson来解析json数据,这样封装起来的库已经足够应付数据量小但通信频繁的网络操作了。下面会给出每个开源库的简介和地址(详细介绍和使用请看官网),接着就进行volley的简单扩展和封装,并且优化部分代码。
简介
Volley
Google出品的一个简化网络任务的库,负责处理请求、加载、缓存、线程、异步等等操作,能处理JSON格式的数据,图片,缓存,纯文字,允许开发者实现一些自定制服务,适合进行数据量不大,但通信频繁的网络操作,使用时最好再进行简单的封装。
源码
非官方库OKhttp3
Square出品的一个高效的HTTP客户端,android 4.4以后已替换掉HttpURLConnection作为默认的HTTP连接,OkHttp 3.x相对于2.x,在api和使用规范上有一些调整。
官网
源码
wikiGson
Google开发的用于转换Java对象和Json对象的java库
源码Jackson
在处理json大文件时解析性能明显优于Gson,如果应用经常需要传输较大的json文件则使用Jackson,小文件则使用Gson。还有阿里的fastjson也有其优势,没用过,后面再说= =
Wiki
下载
Gradle
compile 'com.mcxiaoke.volley:library:1.0.19'
compile 'com.squareup.okhttp3:okhttp:3.1.2'
compile 'com.squareup.okio:okio:1.6.0'
compile 'com.google.code.gson:gson:2.6.1'
简单使用
1、volley的使用一共三步骤,首先获取一个全局的请求队列对象,用来缓存所有的HTTP请求。
RequestQueue mRequestQueue = Volley.newRequestQueue(context);
2、然后新建一个请求,这里用JsonObjectRequest(JsonArrayRequest同理),(接口这里用mockaroo和Mocky在线生成一个)
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://www.mocky.io/v2/56c9d8c9110000c62f4e0bb0", null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
Log.d("mTAG", response.toString());
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("mTAG", error.getMessage(), error);
}
});
3、最后添加请求到队列中
mRequestQueue.add(jsonObjectRequest);
一个网络请求操作就这样方便简单,运行,可以看到log打印如下
{"last_name":"Ramos","id":1,"first_name":"Roger","gender":"Male","ip_address":"194.52.112.37","email":"rramos0@gizmodo.com"}
自定义GsonRequest解析json
为了将上面的json数据解析为Java对象,我们使用Gson库,而velloy没有支持Gson,所以我们仿照JsonObjectRequest自己定义一个GsonRequest
public class GsonRequest<T> extends Request<T> {
private final Listener<T> mListener;
private Gson mGson;
private Class<T> mClass;
public GsonRequest(int method, String url, Class<T> clazz, Listener<T> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mGson = new Gson();
mClass = clazz;
mListener = listener;
}
public GsonRequest(String url, Class<T> clazz, Listener<T> listener,
ErrorListener errorListener) {
this(Method.GET, url, clazz, listener, errorListener);
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data,
HttpHeaderParser.parseCharset(response.headers));
return Response.success(mGson.fromJson(jsonString, mClass),
HttpHeaderParser.parseCacheHeaders(response));//用Gson解析返回Java对象
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
}
}
@Override
protected void deliverResponse(T response) {
mListener.onResponse(response);//回调T对象
}
}
简单分析一下上面代码,我们覆盖了Request
父类的方法,在parseNetworkResponse
中使用了Gson解析得到的jsonString, 然后在deliverResponse
中再回调此T对象。但是,parseNetworkResponse
中Gson的解析只适用单个json对象,如果是json数组呢?所以我们还需要定义一个TypeToken
来提供对复杂类型的支持。
还有一点,就是这个GsonRequest
类只适合get请求,如果是post请求则会去其父类Request
中寻找post参数Params,所以我们再覆盖一下父类的getParams()
方法,并且让其支持在构造器中直接传入Params。
具体看一下代码,修改如下
public class GsonRequest<T> extends Request<T> {
private final Listener<T> mListener;
private static Gson mGson = new Gson();
private Class<T> mClass;
private Map<String, String> mParams;//post Params
private TypeToken<T> mTypeToken;
public GsonRequest(int method, Map<String, String> params, String url, Class<T> clazz, Listener<T> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mClass = clazz;
mListener = listener;
mParams = params;
}
public GsonRequest(int method, Map<String, String> params, String url, TypeToken<T> typeToken, Listener<T> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mTypeToken = typeToken;
mListener = listener;
mParams = params;
}
//get
public GsonRequest(String url, Class<T> clazz, Listener<T> listener, ErrorListener errorListener) {
this(Method.GET, null, url, clazz, listener, errorListener);
}
public GsonRequest(String url, TypeToken<T> typeToken, Listener<T> listener, ErrorListener errorListener) {
this(Method.GET, null, url, typeToken, listener, errorListener);
}
@Override
protected Map<String, String> getParams() throws AuthFailureError {
return mParams;
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
if (mTypeToken == null)
return Response.success(mGson.fromJson(jsonString, mClass),
HttpHeaderParser.parseCacheHeaders(response));//用Gson解析返回Java对象
else
return (Response<T>) Response.success(mGson.fromJson(jsonString, mTypeToken.getType()),
HttpHeaderParser.parseCacheHeaders(response));//通过构造TypeToken让Gson解析成自定义的对象类型
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
}
}
@Override
protected void deliverResponse(T response) {
mListener.onResponse(response);
}
}
定义好以后,我们就可以来new一个GsonRequest请求了。一步步来,先根据网络传输的json字段来定义一个实体类,重新看一下刚才运行打印出来的数据
{"last_name":"Ramos","id":1,"first_name":"Roger","gender":"Male","ip_address":"194.52.112.37","email":"rramos0@gizmodo.com"}
我们可以先取json数据中的first_name,last_name和gender作为Person类的属性
实体类Person
public class Person {
private String gender;
private String first_name;
private String last_name;
public void setGender(String gender) {this.gender = gender;}
public String getGender() { return this.gender;}
public void setFirst_name(String first_name) {this.first_name = first_name;}
public String getFirst_name() {return this.first_name;}
public void setLast_name(String last_name) {this.last_name = last_name;}
public String getLast_name() {return this.last_name;}
}
然后新建一个GsonRequest,可以看到,onResponse
回调方法直接返回了一个person对象,打印其数据验证一下
GsonRequest<Person> gsonRequest = new GsonRequest<Person>(
"http://www.mocky.io/v2/56c9d8c9110000c62f4e0bb0", Person.class,
new Response.Listener<Person>() {
@Override
public void onResponse(Person person) {
Log.d(TAG, "first_name: " + person.getFirst_name());
Log.d(TAG, "last_name: " + person.getLast_name());
Log.d(TAG, "gender: " + person.getGender());
mTextview.setText("first_name: " + person.getFirst_name() + "\n"
+ "last_name: " + person.getLast_name() + "\n" +
"gender: " + person.getGender());
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e(TAG, error.getMessage(), error);
}
});
//添加请求到队列
mRequestQueue.add(gsonRequest);
打印的结果当然是对的,我就不贴了。
好,先休息一下~
嗯,接着说,如果应用经常要传输大文件,那么最好是使用Jackson库解析json,因为它比gson更快,JacksonRequest的定义也同样道理,贴上代码
public class JacksonRequest<T> extends Request<T> {
private final Listener<T> mListener;
private static ObjectMapper objectMapper = new ObjectMapper();
private Class<T> mClass;
private TypeReference<T> mTypeReference;//提供解析复杂JSON数据支持
public JacksonRequest(int method, String url, Class<T> clazz, Listener<T> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mClass = clazz;
mListener = listener;
}
public JacksonRequest(int method, String url, TypeReference<T> typeReference, Listener<T> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mTypeReference = typeReference;
mListener = listener;
}
public JacksonRequest(String url, Class<T> clazz, Listener<T> listener, ErrorListener errorListener) {
this(Method.GET, url, clazz, listener, errorListener);
}
public JacksonRequest(String url, TypeReference<T> typeReference, Listener<T> listener,
ErrorListener errorListener) {
super(Method.GET, url, errorListener);
mTypeReference = typeReference;
mListener = listener;
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
Log.v("mTAG", "json");
if (mTypeReference == null)//使用Jackson默认的方式解析到mClass类对象
return (Response<T>) Response.success(
objectMapper.readValue(jsonString, TypeFactory.rawClass(mClass)),
HttpHeaderParser.parseCacheHeaders(response));
else//通过构造TypeReference让Jackson解析成自定义的对象类型
return (Response<T>) Response.success(objectMapper.readValue(jsonString, mTypeReference),
HttpHeaderParser.parseCacheHeaders(response));
} catch (Exception e) {
return Response.error(new ParseError(e));
}
}
@Override
protected void deliverResponse(T response) {
mListener.onResponse(response);
}
}
因为项目中我使用的是Gson,所以没有把Jackson库一起导入,如果要使用的话当然是二选一了,而不是一起使用,不然项目apk文件该有多大啊
加载图片
volley还有加载网络图片的功能,我们可以new一个ImageRequest来获取一张网络的图片,不过它并没有做缓存处理,所以我们用ImageLoader(volley.toolbox.ImageLoader),volley内部实现了磁盘缓存,不过没有内存缓存,我们可以自己来定义。
1.新建一个ImageLoader,设置ImageListener,然后在get方法中传入url,看代码吧
ImageLoader imageLoader = new ImageLoader(mRequestQueue, new MyImageCache());
ImageLoader.ImageListener listener = ImageLoader.getImageListener(mImageview,
R.mipmap.ic_default, R.mipmap.ic_error);
imageLoader.get("https://d262ilb51hltx0.cloudfront.net/max/800/1*dWGwx6UUjc0tocYzFNBLEw.jpeg",
listener, 800, 800);
2.为了实现图片的内存缓存,我们使用LruCache来实现,自定义一个MyImageCache类继承自ImageCache,然后其构造方法中new一个最大为8M的LruCache
public class MyImageCache implements ImageLoader.ImageCache {
private LruCache<String, Bitmap> mCache;
public MyImageCache() {
int maxSize = 8 * 1024 * 1024;
mCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//getRowBytes()返回图片每行的字节数,乘以高度得到图片的size
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);
}
}
添加OkHttp
我们已经实现了volley+Gson了,如果要使用OkHttp作为传输层,我们只需要在构建 Volley 的请求队列对象requestQueue时做一下改变,将OkHttp3Stack作为参数传进去。OkHttp3Stack具体的实现看一下链接 代码
mRequestQueue = Volley.newRequestQueue(context, new OkHttp3Stack(new OkHttpClient()));
二次封装
最后我们可以把volley的使用封装成一个VolleyManager,代码太长,见这里。
或者也可以把volley的请求操作提取出来放到Application中,这样整个app就只用一个请求队列对象。
public class App extends Application {
public static final String TAG = "App";
public RequestQueue mRequestQueue;//请求队列
private ImageLoader mImageLoader;
private static App mInstance;
@Override
public void onCreate() {
super.onCreate();
mInstance = this;
}
public static synchronized App getInstance() {
return mInstance;
}
public RequestQueue getRequestQueue() {
if (mRequestQueue == null) {
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
}
return mRequestQueue;
}
public ImageLoader getImageLoader() {
getRequestQueue();
if (mImageLoader == null) {
mImageLoader = new ImageLoader(this.mRequestQueue,
new MyImageCache());
}
return this.mImageLoader;
}
public <T> void addRequest(Request<T> req, String tag) {
req.setTag(tag);
getRequestQueue().add(req);
}
public <T> void addRequest(Request<T> req) {
req.setTag(TAG);
getRequestQueue().add(req);
}
public void cancelRequests(Object tag) {
if (mRequestQueue != null) {
mRequestQueue.cancelAll(tag);
}
}
}
优化
1.上面加载图片中MyImageCache
类的图片缓存大小是固定的,改成这个可以实现动态地分配缓存。
public class LruBitmapCache extends LruCache<String, Bitmap>
implements ImageCache {
public LruBitmapCache(int maxSize) {
super(maxSize);
}
public LruBitmapCache(Context ctx) {
this(getCacheSize(ctx));
}
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
public Bitmap getBitmap(String url) {
return get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
put(url, bitmap);
}
// Returns a cache size equal to approximately three screens worth of images.
public static int getCacheSize(Context ctx) {
final DisplayMetrics displayMetrics = ctx.getResources().
getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
// 4 bytes per pixel
final int screenBytes = screenWidth * screenHeight * 4;
return screenBytes * 3;
}
}
2.在自定义的GsonRequest类里,我们可以通过在其构造器中添加 setMyRetryPolicy()
方法来实现请求超时时间的定制。
private void setMyRetryPolicy() {
setRetryPolicy(new DefaultRetryPolicy(30000,
DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
补充
忘了说jackson的导入了= =,为了避免重复入坑,补充一下Jackson的下载
compile 'com.fasterxml.jackson.core:jackson-core:2.7.1'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.1'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1'
记得还要添加一下packagingOptions,因为jackson-core和jackson-databind有重复的文件,重复加载会报错。
android{
...
packagingOptions {
exclude 'META-INF/NOTICE' // will not include NOTICE file
exclude 'META-INF/LICENSE' // will not include LICENSE file
}
}
最后
如果这种解决方案还不满足,还有一种更为强大的,Retrofit+OkHttp,都是Square公司出品,然后图片加载再选择Square的Picasso(或者谷歌推荐的Glide、Facebook的Fresco)。而且,Retrofit还支持RxJava,可以使异步操作的代码更加简洁。这些搭配起来就是网络的神装了。不过Retrofit和RxJava我都没深入研究过,先打好基础再说,以后有时间再看看。
代码已经放上github了,可能有不完善的地方,欢迎一起交流学习
代码地址