前言寒假学习了一下安卓的网络通信部分,扩展和封装了volley,还是挺有意思的,所以写一篇博客来记录一下整个历程吧。大家都知道,安卓网络通信有很多解决方案,比如HttpURLConnection,OkHttp,Android-async-http,Volley等,那为什么是Volley+OkHttp3+Gson(Jackson)?答案是这样的,用volley来进行网络通信,用Okhttp3来处理Volley的底层HTTP请求,然后用Gson或者Jackson来解析json数据,这样封装起来的库已经足够应付数据量小但通信频繁的网络操作了。下面会给出每个开源库的简介和地址(详细介绍和使用请看官网),接着就进行volley的简单扩展和封装,并且优化部分代码。  volley官方演讲配图 简介Volley Google出品的一个简化网络任务的库,负责处理请求、加载、缓存、线程、异步等等操作,能处理JSON格式的数据,图片,缓存,纯文字,允许开发者实现一些自定制服务,适合进行数据量不大,但通信频繁的网络操作,使用时最好再进行简单的封装。 源码 非官方库 OKhttp3 Square出品的一个高效的HTTP客户端,android 4.4以后已替换掉HttpURLConnection作为默认的HTTP连接,OkHttp 3.x相对于2.x,在api和使用规范上有一些调整。 官网 源码 wiki Gson 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./v2/56c9d8c9110000c62f4e0bb0', null, new Response.Listener() { @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 extends Request { private final Listener mListener; private Gson mGson; private Class mClass; public GsonRequest(int method, String url, Class clazz, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); mGson = new Gson(); mClass = clazz; mListener = listener; } public GsonRequest(String url, Class clazz, Listener listener, ErrorListener errorListener) { this(Method.GET, url, clazz, listener, errorListener); } @Override protected Response 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 extends Request { private final Listener mListener; private static Gson mGson = new Gson(); private Class mClass; private Map mParams;//post Params private TypeToken mTypeToken; public GsonRequest(int method, Map params, String url, Class clazz, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); mClass = clazz; mListener = listener; mParams = params; } public GsonRequest(int method, Map params, String url, TypeToken typeToken, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); mTypeToken = typeToken; mListener = listener; mParams = params; } //get public GsonRequest(String url, Class clazz, Listener listener, ErrorListener errorListener) { this(Method.GET, null, url, clazz, listener, errorListener); } public GsonRequest(String url, TypeToken typeToken, Listener listener, ErrorListener errorListener) { this(Method.GET, null, url, typeToken, listener, errorListener); } @Override protected Map getParams() throws AuthFailureError { return mParams; } @Override protected Response 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) 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 gsonRequest = new GsonRequest( 'http://www./v2/56c9d8c9110000c62f4e0bb0', Person.class, new Response.Listener() { @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 extends Request { private final Listener mListener; private static ObjectMapper objectMapper = new ObjectMapper(); private Class mClass; private TypeReference mTypeReference;//提供解析复杂JSON数据支持 public JacksonRequest(int method, String url, Class clazz, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); mClass = clazz; mListener = listener; } public JacksonRequest(int method, String url, TypeReference typeReference, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); mTypeReference = typeReference; mListener = listener; } public JacksonRequest(String url, Class clazz, Listener listener, ErrorListener errorListener) { this(Method.GET, url, clazz, listener, errorListener); } public JacksonRequest(String url, TypeReference typeReference, Listener listener, ErrorListener errorListener) { super(Method.GET, url, errorListener); mTypeReference = typeReference; mListener = listener; } @Override protected Response 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) Response.success( objectMapper.readValue(jsonString, TypeFactory.rawClass(mClass)), HttpHeaderParser.parseCacheHeaders(response)); else//通过构造TypeReference让Jackson解析成自定义的对象类型 return (Response) 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./max/800/1*dWGwx6UUjc0tocYzFNBLEw.jpeg', listener, 800, 800);
2.为了实现图片的内存缓存,我们使用LruCache来实现,自定义一个MyImageCache类继承自ImageCache,然后其构造方法中new一个最大为8M的LruCache public class MyImageCache implements ImageLoader.ImageCache { private LruCache mCache; public MyImageCache() { int maxSize = 8 * 1024 * 1024; mCache = new LruCache(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 void addRequest(Request req, String tag) { req.setTag(tag); getRequestQueue().add(req); } public void addRequest(Request 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 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了,可能有不完善的地方,欢迎一起交流学习 代码地址
|