安卓 Volley+OkHttp3+Gson(Jackson)开源库的封装过程


前言

寒假学习了一下安卓的网络通信部分,扩展和封装了volley,还是挺有意思的,所以写一篇博客来记录一下整个历程吧。大家都知道,安卓网络通信有很多解决方案,比如HttpURLConnection,OkHttp,Android-async-http,Volley等,那为什么是Volley+OkHttp3+Gson(Jackson)?答案是这样的,用volley来进行网络通信,用Okhttp3来处理Volley的底层HTTP请求,然后用Gson或者Jackson来解析json数据,这样封装起来的库已经足够应付数据量小但通信频繁的网络操作了。下面会给出每个开源库的简介和地址(详细介绍和使用请看官网),接着就进行volley的简单扩展和封装,并且优化部分代码。

安卓 Volley+OkHttp3+Gson(Jackson)开源库的封装过程_第1张图片
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.mocky.io/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":"[email protected]"}

自定义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":"[email protected]"}

我们可以先取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.mocky.io/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.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 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<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了,本人新手,可能有不完善的地方,欢迎一起学习交流
代码地址

你可能感兴趣的:(Android)