Android优秀开源RxJAva OKHttp RetrofitAndroid

Android Volley+OkHttp3+Gson(Jack

2016-02-22  本文已影响9436人  Devnan

前言

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

volley官方演讲配图

简介


下载

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


上一篇下一篇

猜你喜欢

热点阅读