前言寒假學習了一下安卓的網(wǎng)絡通信部分,擴展和封裝了volley,還是挺有意思的,所以寫一篇博客來記錄一下整個歷程吧。大家都知道,安卓網(wǎng)絡通信有很多解決方案,比如HttpURLConnection,OkHttp,Android-async-http,Volley等,那為什么是Volley+OkHttp3+Gson(Jackson)?答案是這樣的,用volley來進行網(wǎng)絡通信,用Okhttp3來處理Volley的底層HTTP請求,然后用Gson或者Jackson來解析json數(shù)據(jù),這樣封裝起來的庫已經(jīng)足夠應付數(shù)據(jù)量小但通信頻繁的網(wǎng)絡操作了。下面會給出每個開源庫的簡介和地址(詳細介紹和使用請看官網(wǎng)),接著就進行volley的簡單擴展和封裝,并且優(yōu)化部分代碼。  volley官方演講配圖 簡介Volley Google出品的一個簡化網(wǎng)絡任務的庫,負責處理請求、加載、緩存、線程、異步等等操作,能處理JSON格式的數(shù)據(jù),圖片,緩存,純文字,允許開發(fā)者實現(xiàn)一些自定制服務,適合進行數(shù)據(jù)量不大,但通信頻繁的網(wǎng)絡操作,使用時最好再進行簡單的封裝。 源碼 非官方庫 OKhttp3 Square出品的一個高效的HTTP客戶端,android 4.4以后已替換掉HttpURLConnection作為默認的HTTP連接,OkHttp 3.x相對于2.x,在api和使用規(guī)范上有一些調(diào)整。 官網(wǎng) 源碼 wiki Gson Google開發(fā)的用于轉換Java對象和Json對象的java庫 源碼 Jackson 在處理json大文件時解析性能明顯優(yōu)于Gson,如果應用經(jīng)常需要傳輸較大的json文件則使用Jackson,小文件則使用Gson。還有阿里的fastjson也有其優(yōu)勢,沒用過,后面再說= = 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);
一個網(wǎng)絡請求操作就這樣方便簡單,運行,可以看到log打印如下 {'last_name':'Ramos','id':1,'first_name':'Roger','gender':'Male','ip_address':'194.52.112.37','email':'rramos0@gizmodo.com'}
自定義GsonRequest解析json為了將上面的json數(shù)據(jù)解析為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);//回調(diào)T對象 }}
簡單分析一下上面代碼,我們覆蓋了Request 父類的方法,在parseNetworkResponse 中使用了Gson解析得到的jsonString, 然后在deliverResponse 中再回調(diào)此T對象。但是,parseNetworkResponse 中Gson的解析只適用單個json對象,如果是json數(shù)組呢?所以我們還需要定義一個TypeToken 來提供對復雜類型的支持。 還有一點,就是這個GsonRequest 類只適合get請求,如果是post請求則會去其父類Request 中尋找post參數(shù)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請求了。一步步來,先根據(jù)網(wǎng)絡傳輸?shù)膉son字段來定義一個實體類,重新看一下剛才運行打印出來的數(shù)據(jù) {'last_name':'Ramos','id':1,'first_name':'Roger','gender':'Male','ip_address':'194.52.112.37','email':'rramos0@gizmodo.com'}
我們可以先取json數(shù)據(jù)中的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 回調(diào)方法直接返回了一個person對象,打印其數(shù)據(jù)驗證一下 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); 打印的結果當然是對的,我就不貼了。 好,先休息一下~ 嗯,接著說,如果應用經(jīng)常要傳輸大文件,那么最好是使用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數(shù)據(jù)支持 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還有加載網(wǎng)絡圖片的功能,我們可以new一個ImageRequest來獲取一張網(wǎng)絡的圖片,不過它并沒有做緩存處理,所以我們用ImageLoader(volley.toolbox.ImageLoader),volley內(nèi)部實現(xiàn)了磁盤緩存,不過沒有內(nèi)存緩存,我們可以自己來定義。 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.為了實現(xiàn)圖片的內(nèi)存緩存,我們使用LruCache來實現(xiàn),自定義一個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()返回圖片每行的字節(jié)數(shù),乘以高度得到圖片的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我們已經(jīng)實現(xiàn)了volley+Gson了,如果要使用OkHttp作為傳輸層,我們只需要在構建 Volley 的請求隊列對象requestQueue時做一下改變,將OkHttp3Stack作為參數(shù)傳進去。OkHttp3Stack具體的實現(xiàn)看一下鏈接 代碼 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); } }}
優(yōu)化1.上面加載圖片中MyImageCache 類的圖片緩存大小是固定的,改成這個可以實現(xiàn)動態(tài)地分配緩存。 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() 方法來實現(xiàn)請求超時時間的定制。 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,可以使異步操作的代碼更加簡潔。這些搭配起來就是網(wǎng)絡的神裝了。不過Retrofit和RxJava我都沒深入研究過,先打好基礎再說,以后有時間再看看。
代碼已經(jīng)放上github了,可能有不完善的地方,歡迎一起交流學習 代碼地址
|