Android Volley+OkHttp3+Gson(Jackson)的封装过程


前言

寒假学习了一下安卓的网络通信部分,扩展和封装了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同理),(接口这里用mockarooMocky在线生成一个)

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了,可能有不完善的地方,欢迎一起交流学习
代码地址


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容