HTTP是一個屬於應用層的面向對象的協議,因爲其簡捷、快速的方式,適用於分佈式超媒體信息系統。它於1990年提出,通過幾年的使用與發展,獲得不斷地完善和擴展。php
http://host[":"port][abs_path]
http表示要經過HTTP協議來定位網絡資源;
host表示合法的Internet主機域名或者IP地址;
port指定一個端口號,爲空則使用默認端口80;
abs_path指定請求資源的URI(Web上任意的可用資源)。 html
HTTP有兩種報文分別是請求報文和響應報文,讓咱們先來看看請求報文。java
先來看看請求報文的通常格式:android
一般來講一個HTTP請求報文由請求行、請求報頭、空行、**請求數據**4個部分組成。nginx
請求行由請求方法、URL字段、HTTP協議的版本組成,格式以下:git
Method Request-URI HTTP-Version CRLF
Method表示請求方法;
Request-URI是一個統一資源標識符;
HTTP-Version表示請求的HTTP協議版本;
CRLF表示回車和換行(除了做爲結尾的CRLF外,不容許出現單獨的CR或LF字符)。github
HTTP請求方法有8種,分別是GET、POST、DELETE、PUT、HEAD、TRACE、CONNECT 、OPTIONS。
其中PUT、DELETE、POST、GET分別對應着增刪改查,對於移動開發最經常使用的就是POST和GET了。web
例如我去訪問個人CSDN博客地址請求行是:算法
GET http://blog.csdn.net/itachi85 HTTP/1.1
在請求行以後會有0個或者多個請求報頭,每一個請求報頭都包含一個名字和一個值,它們之間用「:」分割。
關於請求報頭,會在後面的消息報頭一節作統一的解釋。sql
請求頭部會以一個空行,發送回車符和換行符,通知服務器如下不會有請求頭。
請求數據不在GET方法中使用,而是在POST方法中使用。
POST方法適用於須要客戶填寫表單的場合,與請求數據相關的最經常使用的請求頭是Content-Type和Content-Length。
先來看看響應報文的通常格式:
HTTP的響應報文由狀態行、消息報頭、空行、響應正文組成。響應報頭後面會講到,響應正文是服務器返回的資源的內容,先來看看狀態行。
狀態行格式以下:
HTTP-Version Status-Code Reason-Phrase CRLF
HTTP-Version表示服務器HTTP協議的版本;
Status-Code表示服務器發回的響應狀態代碼;
Reason-Phrase表示狀態代碼的文本描述。
狀態代碼有三位數字組成,第一個數字定義了響應的類別,且有五種可能取值:
常見的狀態碼以下:
例如訪問個人CSDN博客地址響應的狀態行是:
HTTP/1.1 200 OK
消息報頭分爲通用報頭、請求報頭、響應報頭、實體報頭等。
消息頭由鍵值對組成,每行一對,關鍵字和值用英文冒號「:」分隔。
既能夠出如今請求報頭,也能夠出如今響應報頭中
請求報頭通知服務器關於客戶端請求的信息,典型的請求頭有:
用於服務器傳遞自身信息的響應,常見的響應報頭:
實體報頭用來定義被傳送資源的信息,既能夠用於請求也可用於響應。
請求和響應消息均可以傳送一個實體,常見的實體報頭爲:
要想查看網頁或者手機請求網絡的請求報文和響應報文有不少種方法,這裏推薦採用Fiddler,在Android利用Fiddler進行網絡數據抓包這篇文章中詳盡介紹瞭如何使用Fiddler,在這裏就不贅述了。
打開Fiddler,而後用瀏覽器訪問個人CSDN博客網站:
點擊紅色畫筆的區域就能夠看到請求報文和響應報文了
請求報文:
GET http://blog.csdn.net/itachi85 HTTP/1.1 //請求行 Host: blog.csdn.net //請求報頭 Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.80 Safari/537.36 QQBrowser/9.3.6872.400 Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8 Cookie: bdshare_firstime=1443768140949; uuid_tt_dd=5028529250430960147_20151002; ...省略
很容易看出訪問的是個人博客地址http://blog.csdn.net/itachi85,請求的方法是GET,由於是GET方法因此並無請求數據。
響應報文:
HTTP/1.1 200 OK //狀態行 Server: openresty //響應報頭 Date: Sun, 27 Mar 2016 08:26:54 GMT Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Keep-Alive: timeout=20 Vary: Accept-Encoding Cache-Control: private X-Powered-By: PHP 5.4.28 Content-Encoding: gzip //不能省略的空格 28b5 }ysI 1ߡFsgl n- ]{^_ { 'z! C , m# 0 !l ` 4x ly .ݪ* ڴzAt_Xl * 9'O ɬ ' ק 3 ^1a ...省略
響應報文沒什麼可說的,接下來咱們配置好手機網絡代理,訪問一個應用的界面
請求報文:
POST http://patientapi.shoujikanbing.com/api/common/getVersion HTTP/1.1 //請求行 Content-Length: 226 //請求報頭 Content-Type: application/x-www-form-urlencoded Host: patientapi.shoujikanbing.com Connection: Keep-Alive User-Agent: Mozilla/5.0 (Linux; U; Android 4.4.4; zh-cn; MI NOTE LTE Build/KTU84P) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 Accept-Encoding: gzip //不能省略的空格,下面是請求數據 clientversion=2_2.0.0&time=1459069342&appId=android&channel=hjwang&sessionId=0d1cee1f31926ffa8894c64804efa855101d56eb21caf5db5dcb9a4955b7fbc9&token=b191944d680145b5ed97f2f4ccf03058&deviceId=869436020220717&type=2&version=2.0.0
從請求報文的請求行來看,請求的方法是POST,請求地址爲http://patientapi.shoujikanbing.com/api/common/getVersion,很顯然是獲取版本信息的接口。
響應報文:
HTTP/1.1 200 OK //狀態行 Server: nginx //響應報頭 Date: Sun, 27 Mar 2016 09:02:20 GMT Content-Type: text/html;charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Set-Cookie: sessionId=0d1cee1f31926ffa8894c64804efa855101d56eb21caf5db5dcb9a4955b7fbc9; expires=Mon, 28-Mar-2016 09:02:20 GMT; Max-Age=86400; path=/; domain=.shoujikanbing.com Set-Cookie: PHPSESSID=0d1cee1f31926ffa8894c64804efa855101d56eb21caf5db5dcb9a4955b7fbc9; path=/; domain=.shoujikanbing.com Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Content-Encoding: gzip //不能省略的空格 17f //實體報文編碼格式爲gzip因此顯示在這裏的響應數據是亂碼 mP N @ "E ? n m 1 w ( HL (1^ P nK E ѷ93'3gNLH 7P $c \ T 4a6 L:+ 1dY%$g h H + ...省略
響應報文的實體採用的編碼格式爲爲gzip,因此在Fiddler軟件中顯示的是亂碼。
一般咱們進行HTTP鏈接網絡的時候咱們會進行TCP的三次握手,而後傳輸數據,而後再釋放鏈接。
TCP三次握手的過程爲:
第一次握手:創建鏈接。客戶端發送鏈接請求報文段,將SYN位置爲1,Sequence Number爲x;而後,客戶端進入SYN_SEND狀態,等待服務器的確認;
第二次握手:服務器收到客戶端的SYN報文段,須要對這個SYN報文段進行確認,設置Acknowledgment Number爲x+1(Sequence Number+1);同時,本身本身還要發送SYN請求信息,將SYN位置爲1,Sequence Number爲y;服務器端將上述全部信息放到一個報文段(即SYN+ACK報文段)中,一併發送給客戶端,此時服務器進入SYN_RECV狀態;
第三次握手:客戶端收到服務器的SYN+ACK報文段。而後將Acknowledgment Number設置爲y+1,向服務器發送ACK報文段,這個報文段發送完畢之後,客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。
當客戶端和服務器經過三次握手創建了TCP鏈接之後,當數據傳送完畢,斷開鏈接就須要進行TCP四次分手:
第一次分手:主機1(可使客戶端,也能夠是服務器端),設置Sequence Number和Acknowledgment
Number,向主機2發送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有數據要發送給主機2了;
第二次分手:主機2收到了主機1發送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number爲Sequence
第三次分手:主機2向主機1發送FIN報文段,請求關閉鏈接,同時主機2進入LAST_ACK狀態;
第四次分手:主機1收到主機2發送的FIN報文段,向主機2發送ACK報文段,而後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段之後,就關閉鏈接;此時,主機1等待2MSL後依然沒有收到回覆,則證實Server端已正常關閉,那好,主機1也能夠關閉鏈接了。
來看下面的圖增強下理解:
固然大量的鏈接每次鏈接關閉都要三次握手四次分手的很顯然會形成性能低下,所以http有一種叫作keepalive connections的機制,它能夠在傳輸數據後仍然保持鏈接,當客戶端須要再次獲取數據時,直接使用剛剛空閒下來的鏈接而不須要再次握手。
Apache的HttpClient和Java的HttpURLConnection,這兩種都是咱們日常請求網絡會用到的。不管咱們是本身封裝的網絡請求類仍是第三方的網絡請求框架都離不開這兩個類庫。
Android SDK中包含了HttpClient,在Android6.0版本直接刪除了HttpClient類庫,若是仍想使用則解決方法是:
android { useLibrary 'org.apache.http.legacy' }
首先咱們來用DefaultHttpClient類來實例化一個HttpClient,並配置好默認的請求參數:
//建立HttpClient private HttpClient createHttpClient() { HttpParams mDefaultHttpParams = new BasicHttpParams(); //設置鏈接超時 HttpConnectionParams.setConnectionTimeout(mDefaultHttpParams, 15000); //設置請求超時 HttpConnectionParams.setSoTimeout(mDefaultHttpParams, 15000); HttpConnectionParams.setTcpNoDelay(mDefaultHttpParams, true); HttpProtocolParams.setVersion(mDefaultHttpParams, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(mDefaultHttpParams, HTTP.UTF_8); //持續握手 HttpProtocolParams.setUseExpectContinue(mDefaultHttpParams, true); HttpClient mHttpClient = new DefaultHttpClient(mDefaultHttpParams); return mHttpClient; }
接下來建立HttpGet和HttpClient,請求網絡並獲得HttpResponse,並對HttpResponse進行處理:
private void useHttpClientGet(String url) { HttpGet mHttpGet = new HttpGet(url);//建立HttpGet mHttpGet.addHeader("Connection", "Keep-Alive"); try { HttpClient mHttpClient = createHttpClient();//建立HttpClient HttpResponse mHttpResponse = mHttpClient.execute(mHttpGet);//請求網絡並獲得HttpResponse //處理HttpResponse HttpEntity mHttpEntity = mHttpResponse.getEntity(); int code = mHttpResponse.getStatusLine().getStatusCode(); if (null != mHttpEntity) { InputStream mInputStream = mHttpEntity.getContent(); String respose = converStreamToString(mInputStream); Log.i("wangshu", "請求狀態碼:" + code + "\n請求結果:\n" + respose); mInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } }
converStreamToString方法將請求結果轉換成String類型:
private String converStreamToString(InputStream is) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuffer sb = new StringBuffer(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line + "\n"); } String respose = sb.toString(); return respose; }
最後咱們開啓線程訪問百度:
new Thread(new Runnable() { @Override public void run() { useHttpClientGet("http://www.baidu.com"); } }).start();
請求的返回結果,請求狀態碼爲200,結果就是個html頁,這裏只截取了部分html代碼:
GET請求的參數暴露在URL中,這有些不大穩當,並且URL的長度也有限制:長度在2048字符以內,在HTTP 1.1後URL長度纔沒有限制。通常狀況下POST能夠替代GET,接下來咱們來看看HttpClient的POST請求。
post請求和get相似就是須要配置要傳遞的參數:
private void useHttpClientPost(String url) { HttpPost mHttpPost = new HttpPost(url); mHttpPost.addHeader("Connection", "Keep-Alive"); try { HttpClient mHttpClient = createHttpClient(); List<NameValuePair> postParams = new ArrayList<>(); //要傳遞的參數 postParams.add(new BasicNameValuePair("username", "moon")); postParams.add(new BasicNameValuePair("password", "123")); mHttpPost.setEntity(new UrlEncodedFormEntity(postParams)); HttpResponse mHttpResponse = mHttpClient.execute(mHttpPost); HttpEntity mHttpEntity = mHttpResponse.getEntity(); int code = mHttpResponse.getStatusLine().getStatusCode(); if (null != mHttpEntity) { InputStream mInputStream = mHttpEntity.getContent(); String respose = converStreamToString(mInputStream); Log.i("wangshu", "請求狀態碼:" + code + "\n請求結果:\n" + respose); mInputStream.close(); } } catch (IOException e) { e.printStackTrace(); } }
Android 2.2版本以前,HttpURLConnection一直存在着一些使人厭煩的bug。好比說對一個可讀的InputStream調用close()方法時,就有可能會致使鏈接池失效了。那麼咱們一般的解決辦法就是直接禁用掉鏈接池的功能:
private void disableConnectionReuseIfNecessary() { // 這是一個2.2版本以前的bug if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) { System.setProperty("http.keepAlive", "false"); } }
因此在Android 2.2版本以及以前的版本使用HttpClient是較好的選擇,而在Android 2.3版本及之後,HttpURLConnection則是最佳的選擇,它的API簡單,體積較小,於是很是適用於Android項目。
壓縮和緩存機制能夠有效地減小網絡訪問的流量,在提高速度和省電方面也起到了較大的做用。
另外在Android 6.0版本中,HttpClient庫被移除了,HttpURLConnection則是之後咱們惟一的選擇。
由於會了HttpURLConnection的POST請求那GET請求也就會了,因此我這裏只舉出POST的例子
首先咱們建立一個UrlConnManager類,而後裏面提供getHttpURLConnection()方法用於配置默認的參數並返回HttpURLConnection:
public static HttpURLConnection getHttpURLConnection(String url){ HttpURLConnection mHttpURLConnection=null; try { URL mUrl=new URL(url); mHttpURLConnection=(HttpURLConnection)mUrl.openConnection(); //設置連接超時時間 mHttpURLConnection.setConnectTimeout(15000); //設置讀取超時時間 mHttpURLConnection.setReadTimeout(15000); //設置請求參數 mHttpURLConnection.setRequestMethod("POST"); //添加Header mHttpURLConnection.setRequestProperty("Connection","Keep-Alive"); //接收輸入流 mHttpURLConnection.setDoInput(true); //傳遞參數時須要開啓 mHttpURLConnection.setDoOutput(true); } catch (IOException e) { e.printStackTrace(); } return mHttpURLConnection ; }
由於咱們要發送POST請求,因此在UrlConnManager類中再寫一個postParams()方法用來組織一下請求參數並將請求參數寫入到輸出流中:
public static void postParams(OutputStream output,List<NameValuePair>paramsList) throws IOException{ StringBuilder mStringBuilder=new StringBuilder(); for (NameValuePair pair:paramsList){ if(!TextUtils.isEmpty(mStringBuilder)){ mStringBuilder.append("&"); } mStringBuilder.append(URLEncoder.encode(pair.getName(),"UTF-8")); mStringBuilder.append("="); mStringBuilder.append(URLEncoder.encode(pair.getValue(),"UTF-8")); } BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(output,"UTF-8")); writer.write(mStringBuilder.toString()); writer.flush(); writer.close(); }
接下來咱們添加請求參數,調用postParams()方法將請求的參數組織好傳給HttpURLConnection的輸出流,請求鏈接並處理返回的結果:
private void useHttpUrlConnectionPost(String url) { InputStream mInputStream = null; HttpURLConnection mHttpURLConnection = UrlConnManager.getHttpURLConnection(url); try { List<NameValuePair> postParams = new ArrayList<>(); //要傳遞的參數 postParams.add(new BasicNameValuePair("username", "moon")); postParams.add(new BasicNameValuePair("password", "123")); //HttpURLConnection.getOutputStream輸出流 請求 UrlConnManager.postParams(mHttpURLConnection.getOutputStream(), postParams); //HttpURLConnection.connect 鏈接 mHttpURLConnection.connect(); ////HttpURLConnection.getInputStream輸入流 響應 mInputStream = mHttpURLConnection.getInputStream(); int code = mHttpURLConnection.getResponseCode(); String respose = converStreamToString(mInputStream); Log.i("wangshu", "請求狀態碼:" + code + "\n請求結果:\n" + respose); mInputStream.close(); } catch (IOException e) { e.printStackTrace(); } }
最後開啓線程請求網絡:
private void useHttpUrlConnectionGetThread() { new Thread(new Runnable() { @Override public void run() { useHttpUrlConnectionPost("http://www.baidu.com"); } }).start(); }
這裏咱們仍舊請求百度,看看會發生什麼?
mInputStream = mHttpURLConnection.getInputStream() 這句代碼報錯了,找不到文件。打開Fiddler來分析一下,不瞭解Fiddler和HTTP協議原理的請查看Android網絡編程(一)HTTP協議原理這篇文章。
咱們的請求報文:
看來請求報文沒有問題,再來看看響應報文:
報504錯誤,讀取響應的數據報錯,對於咱們此次請求服務端不能返回完整的響應,返回的數據爲0 bytes,因此mHttpURLConnection.getInputStream() 也讀不到服務端響應的輸入流。固然此次錯誤是正常的,百度沒理由處理咱們的此次POST請求。
在2013年Google I/O大會上推出了一個新的網絡通訊框架Volley。
Volley既能夠訪問網絡取得數據,也能夠加載圖片,而且在性能方面也進行了大幅度的調整。
它的設計目標就是很是適合去進行數據量不大,但通訊頻繁的網絡操做,
而對於大數據量的網絡操做,好比說下載文件等,Volley的表現就會很是糟糕。
在使用Volley前請下載Volley庫並放在libs目錄下並add到工程中。 下載Volley請點擊這
Volley請求網絡都是基於請求隊列的,開發者只要把請求放在請求隊列中就能夠了,請求隊列會依次進行請求,通常狀況下,一個應用程序若是網絡請求沒有特別頻繁則徹底能夠只有一個請求隊列(對應Application),若是很是多或其餘狀況,則能夠是一個Activity對應一個網絡請求隊列,這就要看具體狀況了,首先建立隊列Volley.newRequestQueue(context)
:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
StringRequest返回的數據是String類型的,咱們查看下StringRequest的源碼:
public class StringRequest extends Request<String> { private final Listener<String> mListener; public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) { super(method, url, errorListener); this.mListener = listener; } public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { this(0, url, listener, errorListener); } ... }
有兩個構造函數,其中第一個比第二個多了一個請求的方法,若是採用第二個則默認是GET請求。好了,咱們試着用GET方法來請求百度:
//建立請求隊列 RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); //StringRequest StringRequest mStringRequest = new StringRequest(Request.Method.GET, "http://www.baidu.com", new Response.Listener<String>() { @Override public void onResponse(String response) { Log.i("wangshu", response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } }); //將請求添加在請求隊列中 mQueue.add(mStringRequest);
固然別忘了添加網絡訪問權限:
<uses-permission android:name="android.permission.INTERNET"/>
請求結果不用說是百度界面的html文件:
和StringRequest相似,咱們直接上代碼:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
//JsonRequest JsonObjectRequest mJsonObjectRequest = new JsonObjectRequest(Request.Method.POST,"http://api.1-blog.com/biz/bizserver/article/list.do", new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Log.d("wangshu", response.toString()); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } } ); mQueue.add(mJsonObjectRequest);
爲了解析這些Json數據,咱們用Gson來解析Json數據。
點擊這裏下載Gson將jar包放在libs目錄下並add進工程中。
咱們開始寫article類用於存儲數據:
public class Article { private String desc; private String status; private List<detail> detail = new ArrayList<detail>(); public List<Article.detail> getDetail() { return detail; } public void setDetail(List<Article.detail> detail) { this.detail = detail; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public class detail { private String title; private String article_url; private String my_abstract; private String article_type; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getArticle_url() { return article_url; } public void setArticle_url(String article_url) { this.article_url = article_url; } public String getMy_abstract() { return my_abstract; } public void setMy_abstract(String my_abstract) { this.my_abstract = my_abstract; } public String getArticle_type() { return article_type; } public void setArticle_type(String article_type) { this.article_type = article_type; } } }
最後咱們改寫JsonRequest的請求回調:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
JsonObjectRequest mJsonObjectRequest = new JsonObjectRequest(Request.Method.POST,"http://api.1-blog.com/biz/bizserver/article/list.do", new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { //new Gson().fromJson 把json轉成bean Article mArticle=new Gson().fromJson(response.toString(), Article.class); List<Article.detail>mList=mArticle.getDetail(); String title=mList.get(0).getTitle(); Log.d("wangshu", title); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } } ); mQueue.add(mJsonObjectRequest);
來看看打印結果:
ImageRequest已是過期的方法了,和前面兩種的用法相似:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
//ImageRequest ImageRequest imageRequest = new ImageRequest( "http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg", new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { iv_image.setImageBitmap(response); } }, 0, 0, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { iv_image.setImageResource(R.drawable.ico_default); } }); mQueue.add(imageRequest);
查看ImageRequest的源碼發現它能夠設置你想要的圖片的最大寬度和高度,在加載圖片時若是圖片超過時望的最大寬度和高度則會進行壓縮:
public ImageRequest(String url, Listener<Bitmap> listener, int maxWidth, int maxHeight, ScaleType scaleType, Config decodeConfig, ErrorListener errorListener) { super(0, url, errorListener); this.setRetryPolicy(new DefaultRetryPolicy(1000, 2, 2.0F)); this.mListener = listener; this.mDecodeConfig = decodeConfig; this.mMaxWidth = maxWidth; this.mMaxHeight = maxHeight; this.mScaleType = scaleType; } //壓縮圖片 private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; Options decodeOptions = new Options(); Bitmap bitmap = null; if(this.mMaxWidth == 0 && this.mMaxHeight == 0) { decodeOptions.inPreferredConfig = this.mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { decodeOptions.inJustDecodeBounds = true; BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); int actualWidth = decodeOptions.outWidth; int actualHeight = decodeOptions.outHeight; int desiredWidth = getResizedDimension(this.mMaxWidth, this.mMaxHeight, actualWidth, actualHeight, this.mScaleType); int desiredHeight = getResizedDimension(this.mMaxHeight, this.mMaxWidth, actualHeight, actualWidth, this.mScaleType); decodeOptions.inJustDecodeBounds = false; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); if(tempBitmap == null || tempBitmap.getWidth() <= desiredWidth && tempBitmap.getHeight() <= desiredHeight) { bitmap = tempBitmap; } else { bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); tempBitmap.recycle(); } } return bitmap == null?Response.error(new ParseError(response)):Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); } private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary, ScaleType scaleType) { if(maxPrimary == 0 && maxSecondary == 0) { return actualPrimary; } else if(scaleType == ScaleType.FIT_XY) { return maxPrimary == 0?actualPrimary:maxPrimary; } else { double ratio; if(maxPrimary == 0) { ratio = (double)maxSecondary / (double)actualSecondary; return (int)((double)actualPrimary * ratio); } else if(maxSecondary == 0) { return maxPrimary; } else { ratio = (double)actualSecondary / (double)actualPrimary; int resized = maxPrimary; if(scaleType == ScaleType.CENTER_CROP) { if((double)maxPrimary * ratio < (double)maxSecondary) { resized = (int)((double)maxSecondary / ratio); } return resized; } else { if((double)maxPrimary * ratio > (double)maxSecondary) { resized = (int)((double)maxSecondary / ratio); } return resized; } } } } static int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { double wr = (double)actualWidth / (double)desiredWidth; double hr = (double)actualHeight / (double)desiredHeight; double ratio = Math.min(wr, hr); float n; for(n = 1.0F; (double)(n * 2.0F) <= ratio; n *= 2.0F) { ; } return (int)n; }
ImageLoader的內部使用ImageRequest來實現,它的構造器能夠傳入一個ImageCache緩存形參,實現了圖片緩存的功能,同時還能夠過濾重複連接,避免重複發送請求。
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
//ImageLoader ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache()); ImageLoader.ImageListener listener = ImageLoader.getImageListener(iv_image,R.drawable.ico_default, R.drawable.ico_default); imageLoader.get("http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg", listener);
與ImageRequest實現效果不一樣的是,ImageLoader加載圖片會先顯示默認的圖片,等待圖片加載完成纔會顯示在ImageView上。
固然ImageLoader也提供了設置最大寬度和高度的方法:
public ImageLoader.ImageContainer get(String requestUrl, ImageLoader.ImageListener imageListener, int maxWidth, int maxHeight) { return this.get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); }
NetworkImageView是一個自定義控件,繼承自ImageView,封裝了請求網絡加載圖片的功能。
先在佈局中引用:
<com.android.volley.toolbox.NetworkImageView android:id="@+id/nv_image" android:layout_width="200dp" android:layout_height="200dp" android:layout_centerHorizontal="true" android:layout_below="@id/iv_image" android:layout_marginTop="20dp" ></com.android.volley.toolbox.NetworkImageView>
代碼中調用,和ImageLoader用法相似:
iv_image = (ImageView) this.findViewById(R.id.iv_image); RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext()); ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache()); nv_image.setDefaultImageResId(R.drawable.ico_default); nv_image.setErrorImageResId(R.drawable.ico_default); nv_image.setImageUrl("http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg", imageLoader);
NetworkImageView並無提供設置最大寬度和高度的方法,根據咱們設置控件的寬和高結合網絡圖片的寬和高內部會自動去實現壓縮,若是咱們不想要壓縮能夠設置NetworkImageView控件的寬和高都爲wrap_content。
從上圖能夠看到Volley分爲三個線程,分別是主線程、緩存調度線程、和網絡調度線程。
首先請求會加入緩存隊列,若是發現能夠找到相應的緩存結果就直接讀取緩存並解析,而後回調給主線程;
若是在緩存中沒有找到結果,則將這條請求加入到網絡隊列中,而後發送HTTP請求,解析響應並寫入緩存,並回調給主線程。
咱們都知道使用Volley以前首先要建立RequestQueue:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
這也是volley運做的入口,看看newRequestQueue:
public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, (HttpStack)null); } public static RequestQueue newRequestQueue(Context context, HttpStack stack) { return newRequestQueue(context, stack, -1); }
連續調用了兩個重載函數,最終調用的是:
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) { File cacheDir = new File(context.getCacheDir(), "volley"); String userAgent = "volley/0"; try { String network = context.getPackageName(); PackageInfo queue = context.getPackageManager().getPackageInfo(network, 0); userAgent = network + "/" + queue.versionCode; } catch (NameNotFoundException var7) { ; } if(stack == null) { if(VERSION.SDK_INT >= 9) { stack = new HurlStack(); } else { stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); } } BasicNetwork network1 = new BasicNetwork((HttpStack)stack); RequestQueue queue1; if(maxDiskCacheBytes <= -1) { queue1 = new RequestQueue(new DiskBasedCache(cacheDir), network1); } else { queue1 = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network1); } queue1.start(); return queue1; }
能夠看到若是android版本大於等於2.3則調用基於HttpURLConnection的HurlStack,不然就調用基於HttpClient的HttpClientStack。
並建立了RequestQueue,調用了start()方法:
public void start() { this.stop(); this.mCacheDispatcher = new CacheDispatcher(this.mCacheQueue, this.mNetworkQueue, this.mCache, this.mDelivery); this.mCacheDispatcher.start(); for(int i = 0; i < this.mDispatchers.length; ++i) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(this.mNetworkQueue, this.mNetwork, this.mCache, this.mDelivery); this.mDispatchers[i] = networkDispatcher; networkDispatcher.start(); } }
CacheDispatcher是緩存調度線程,並調用了start()方法,
在循環中調用了NetworkDispatcher的start()方法,NetworkDispatcher是網絡調度線程,默認狀況下mDispatchers.length爲4,默認開啓了4個網絡調度線程,也就是說有5個線程在後臺運行並等待請求的到來。
接下來咱們建立各類的Request,並調用RequestQueue的add()方法:
public Request add(Request request) { request.setRequestQueue(this); Set var2 = this.mCurrentRequests; synchronized(this.mCurrentRequests) { this.mCurrentRequests.add(request); } request.setSequence(this.getSequenceNumber()); request.addMarker("add-to-queue"); //若是不能緩存,則將請求添加到網絡請求隊列中 if(!request.shouldCache()) { this.mNetworkQueue.add(request); return request; } else { Map var8 = this.mWaitingRequests; synchronized(this.mWaitingRequests) { String cacheKey = request.getCacheKey(); //以前是否有執行相同的請求且尚未返回結果的,若是有的話將此請求加入mWaitingRequests隊列,再也不重複請求 if(this.mWaitingRequests.containsKey(cacheKey)) { Object stagedRequests = (Queue)this.mWaitingRequests.get(cacheKey); if(stagedRequests == null) { stagedRequests = new LinkedList(); } ((Queue)stagedRequests).add(request); this.mWaitingRequests.put(cacheKey, stagedRequests); if(VolleyLog.DEBUG) { VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", new Object[]{cacheKey}); } } else { //沒有的話就將請求加入緩存隊列mCacheQueue,同時加入mWaitingRequests中用來作下次一樣請求來時的重複判斷依據 this.mWaitingRequests.put(cacheKey, (Object)null); this.mCacheQueue.add(request); } return request; } } }
經過判斷request.shouldCache(),來判斷是否能夠緩存,默認是能夠緩存的,
若是不能緩存,則將請求添加到網絡請求隊列中,
若是能緩存,就判斷以前是否有執行相同的請求且尚未返回結果的,
若是有的話將此請求加入mWaitingRequests隊列,再也不重複請求;
沒有的話就將請求加入緩存隊列mCacheQueue,同時加入mWaitingRequests中用來作下次一樣請求來時的重複判斷依據。
從上面能夠看出RequestQueue的add()方法並無作什麼請求網絡或者對緩存進行操做。
當將請求添加到網絡請求隊列或者緩存隊列時,這時在後臺的網絡調度線程和緩存調度線程輪詢各自的請求隊列發現有請求任務則開始執行,咱們先看看緩存調度線程。
CacheDispatcher的run()方法:
public void run() { if(DEBUG) { VolleyLog.v("start new dispatcher", new Object[0]); } //線程優先級設置爲最高級別 Process.setThreadPriority(10); this.mCache.initialize(); while(true) { while(true) { while(true) { while(true) { try { //獲取緩存隊列中的一個請求 final Request e = (Request)this.mCacheQueue.take(); e.addMarker("cache-queue-take"); //若是請求取消了則將請求中止掉 if(e.isCanceled()) { e.finish("cache-discard-canceled"); } else { //查看是否有緩存的響應 Entry entry = this.mCache.get(e.getCacheKey()); //若是緩存響應爲空,則將請求加入網絡請求隊列 if(entry == null) { e.addMarker("cache-miss"); this.mNetworkQueue.put(e); //判斷緩存響應是否過時 } else if(!entry.isExpired()) { e.addMarker("cache-hit"); //對數據進行解析並回調給主線程 Response response = e.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders)); e.addMarker("cache-hit-parsed"); if(!entry.refreshNeeded()) { this.mDelivery.postResponse(e, response); } else { e.addMarker("cache-hit-refresh-needed"); e.setCacheEntry(entry); response.intermediate = true; this.mDelivery.postResponse(e, response, new Runnable() { public void run() { try { CacheDispatcher.this.mNetworkQueue.put(e); } catch (InterruptedException var2) { ; } } }); } } else { e.addMarker("cache-hit-expired"); e.setCacheEntry(entry); this.mNetworkQueue.put(e); } } } catch (InterruptedException var4) { if(this.mQuit) { return; } } } } } } } static { DEBUG = VolleyLog.DEBUG; }
看到四個while循環有些暈吧,讓咱們挑重點的說:
首先從緩存隊列取出請求,判斷請求是否被取消了,
若是沒有,則判斷該請求是否有緩存的響應,
若是有而且沒有過時則對緩存響應進行解析並回調給主線程,
不然,將請求加入網絡請求隊列。
接下來看看網絡調度線程。
NetworkDispatcher的run()方法:
public void run() { Process.setThreadPriority(10); while(true) { long startTimeMs; Request request; while(true) { startTimeMs = SystemClock.elapsedRealtime(); try { //從隊列中取出請求 request = (Request)this.mQueue.take(); break; } catch (InterruptedException var6) { if(this.mQuit) { return; } } } try { request.addMarker("network-queue-take"); if(request.isCanceled()) { request.finish("network-discard-cancelled"); } else { this.addTrafficStatsTag(request); //請求網絡 NetworkResponse e = this.mNetwork.performRequest(request); request.addMarker("network-http-complete"); if(e.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); } else { Response volleyError1 = request.parseNetworkResponse(e); request.addMarker("network-parse-complete"); if(request.shouldCache() && volleyError1.cacheEntry != null) { //將響應結果存入緩存 this.mCache.put(request.getCacheKey(), volleyError1.cacheEntry); request.addMarker("network-cache-written"); } request.markDelivered(); this.mDelivery.postResponse(request, volleyError1); } } } catch (VolleyError var7) { var7.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); this.parseAndDeliverNetworkError(request, var7); } catch (Exception var8) { VolleyLog.e(var8, "Unhandled exception %s", new Object[]{var8.toString()}); VolleyError volleyError = new VolleyError(var8); volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); this.mDelivery.postError(request, volleyError); } } }
網絡調度線程也是從隊列中取出請求而且判斷是否被取消了,
若是沒取消,就去請求網絡獲得響應並回調給主線程。
請求網絡時調用this.mNetwork.performRequest(request)
,這個mNetwork是一個接口,實現它的類是BasicNetwork,咱們來看看BasicNetwork的performRequest()方法:
public NetworkResponse performRequest(Request<?> request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while(true) { HttpResponse httpResponse = null; Object responseContents = null; Map responseHeaders = Collections.emptyMap(); try { HashMap e = new HashMap(); this.addCacheHeaders(e, request.getCacheEntry()); //調用HttpStack的performRequest()方法請求網絡 httpResponse = this.mHttpStack.performRequest(request, e); StatusLine statusCode1 = httpResponse.getStatusLine(); int networkResponse1 = statusCode1.getStatusCode(); responseHeaders = convertHeaders(httpResponse.getAllHeaders()); //根據不一樣的響應狀態碼來返回不一樣的NetworkResponse if(networkResponse1 == 304) { Entry requestLifetime2 = request.getCacheEntry(); if(requestLifetime2 == null) { return new NetworkResponse(304, (byte[])null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } requestLifetime2.responseHeaders.putAll(responseHeaders); return new NetworkResponse(304, requestLifetime2.data, requestLifetime2.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } ...
從上面能夠看到在12行調用的是HttpStack的performRequest()方法請求網絡,接下來根據不一樣的響應狀態碼來返回不一樣的NetworkResponse。
另外HttpStack也是一個接口,實現它的兩個類咱們在前面已經提到了就是HurlStack和HttpClientStack。
讓咱們再回到NetworkDispatcher,請求網絡後,會將響應結果存在緩存中,若是響應結果成功則調用this.mDelivery.postResponse(request, volleyError1)來回調給主線程。
來看看Delivery的postResponse()方法:
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) { request.markDelivered(); request.addMarker("post-response"); this.mResponsePoster.execute(new ExecutorDelivery.ResponseDeliveryRunnable(request, response, runnable)); }
來看看ResponseDeliveryRunnable裏面作了什麼:
private class ResponseDeliveryRunnable implements Runnable { private final Request mRequest; private final Response mResponse; private final Runnable mRunnable; public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { this.mRequest = request; this.mResponse = response; this.mRunnable = runnable; } public void run() { if(this.mRequest.isCanceled()) { this.mRequest.finish("canceled-at-delivery"); } else { if(this.mResponse.isSuccess()) { //deliverResponse this.mRequest.deliverResponse(this.mResponse.result); } else { this.mRequest.deliverError(this.mResponse.error); } if(this.mResponse.intermediate) { this.mRequest.addMarker("intermediate-response"); } else { this.mRequest.finish("done"); } if(this.mRunnable != null) { this.mRunnable.run(); } } } }
第17行調用了this.mRequest.deliverResponse(this.mResponse.result)。deliverResponse
就是實現Request抽象類必需要實現的方法,咱們來看看StringRequest的源碼:
public class StringRequest extends Request { private final Listener mListener; public StringRequest(int method, String url, Listener listener, ErrorListener errorListener) { super(method, url, errorListener); this.mListener = listener; } public StringRequest(String url, Listener listener, ErrorListener errorListener) { this(0, url, listener, errorListener); } protected void deliverResponse(String response) { //最終將response回調給了Response.Listener的onResponse()方法 this.mListener.onResponse(response); } ... }
在deliverResponse方法中調用了this.mListener.onResponse(response),最終將response回調給了Response.Listener的onResponse()方法。
咱們用StringRequest請求網絡的寫法是這樣的:
RequestQueue mQueue = Volley.newRequestQueue(getApplicationContext());
StringRequest mStringRequest = new StringRequest(Request.Method.GET, "http://www.baidu.com", new Response.Listener() { @Override public void onResponse(String response) { Log.i("wangshu", response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("wangshu", error.getMessage(), error); } }); //將請求添加在請求隊列中 mQueue.add(mStringRequest);
咱們接下來看看目前比較火的網絡框架OkHttp, 它處理了不少網絡疑難雜症:會從不少經常使用的鏈接問題中自動恢復。
若是您的服務器配置了多個IP地址,當第一個IP鏈接失敗的時候,OkHttp會自動嘗試下一個IP,此外OkHttp還處理了代理服務器問題和SSL握手失敗問題。
eclipse引入jar包地址:
okhttp-2.7.5.jar
okio-1.7.0.jar
Android Studio 配置gradle:
compile 'com.squareup.okhttp:okhttp:2.7.5' compile 'com.squareup.okio:okio:1.7.0'
基本的步驟很簡單:
- ① 建立OkHttpClient
OkHttpClient mOkHttpClient = new OkHttpClient();
final Request request = new Request.Builder() .url("http://www.baidu.com") .build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) throws IOException { } });
可是每次這麼寫確定是很麻煩,確定是要進行封裝的。須要注意的是onResponse回調並非在UI線程。
最簡單的get請求,老規矩請求百度:
private void getAsynHttp() { //① 建立okHttpClient對象 OkHttpClient mOkHttpClient = new OkHttpClient(); //② 建立Request final Request request = new Request.Builder() .url("http://www.baidu.com") .build(); //③ 建立Call Call call = mOkHttpClient.newCall(request); //④ 調用Call的enqueue 進行異步請求 call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) throws IOException { //onResponse回調並非在UI String str = response.body().string(); Log.i("wangshu", str); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplication(), "請求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
運行程序log打印出來的是百度首頁的html文件。
同步Get請求和異步調用區別就是調用了call的execute()方法。
private String getSyncHttp() throws IOException{ //① 建立okHttpClient對象 OkHttpClient mOkHttpClient = new OkHttpClient(); //② 建立Request final Request request = new Request.Builder() .url("http://www.baidu.com") .build(); //③ 建立Call Call call = mOkHttpClient.newCall(request); //④ 調用Call的execute 進行同步請求 Response mResponse=call.execute(); if (mResponse.isSuccessful()) { return mResponse.body().string(); } else { throw new IOException("Unexpected code " + mResponse); } }
post與get不一樣的就是要建立RequestBody = new FormEncodingBuilder
().add(「size」, 「10」).build(); 並傳進Request中,一樣onResponse回調不是在UI線程。
private void postAsynHttp() { OkHttpClient mOkHttpClient = new OkHttpClient(); //建立RequestBody RequestBody formBody = new FormEncodingBuilder() .add("size", "10") .build(); Request request = new Request.Builder() .url("http://api.1-blog.com/biz/bizserver/article/list.do") .post(formBody)//RequestBody傳進Request中 .build(); Call call = mOkHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(Response response) throws IOException { String str = response.body().string(); Log.i("wangshu", str); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "請求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
首先咱們設置緩存路徑和大小並設置給OkHttpClient: mOkHttpClient.setCache
mOkHttpClient = new OkHttpClient(); File sdcache = getExternalCacheDir(); int cacheSize = 10 * 1024 * 1024; mOkHttpClient.setCache(new Cache(sdcache.getAbsoluteFile(), cacheSize));
接下來異步GET請求baidu:
private void getAsynHttp() { //建立請求Request final Request request = new Request.Builder() .url("http://www.baidu.com") .build(); Call call = mOkHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) throws IOException { if (null != response.cacheResponse()) { String str = response.cacheResponse().toString(); Log.i("wangshu", "cache---" + str); } else { response.body().string(); String str=response.networkResponse().toString(); Log.i("wangshu", "network---" + str); } runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "請求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
第一次請求會請求網絡獲得數據,第二次以及後面的請求則會從緩存中取出數據:
固然也有種狀況是有的請求每次都須要最新的數據,則在建立Request,來設置cacheControl
爲CacheControl.FORCE_NETWORK
,用來表示請求會一直請求網絡獲得數據:
final Request request = new Request.Builder() .url("http://www.baidu.com") .cacheControl(CacheControl.FORCE_NETWORK) .build();
運行程序結果爲:
另外咱們也須要設置超時的時間用來處理各類網絡超時的狀況,超時的緣由多是網絡問題也多是服務器響應慢等問題,OkHttp固然不會忽略這一點,它支持鏈接、讀取和寫入超時的時間設置:mOkHttpClient.setConnectTimeout
/.setWriteTimeout
/.setReadTimeout
mOkHttpClient = new OkHttpClient(); mOkHttpClient.setConnectTimeout(15, TimeUnit.SECONDS); mOkHttpClient.setWriteTimeout(20, TimeUnit.SECONDS); mOkHttpClient.setReadTimeout(20, TimeUnit.SECONDS);
使用call.cancel()
能夠當即中止掉一個正在執行的call。
若是一個線程正在寫請求或者讀響應,將會引起IOException。
當用戶離開一個應用時或者跳到其餘界面時,使用Call.cancel()能夠節約網絡資源,另外無論同步仍是異步的call均可以取消。
也能夠經過tags來同時取消多個請求。當你構建一請求時,使用RequestBuilder.tag(tag)
來分配一個標籤。以後你就能夠用OkHttpClient.cancel(tag)
來取消全部帶有這個tag的call。
爲了模擬這個場景咱們首先建立一個定時的線程池:
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
接下來的代碼爲:
private void cancel(){ final Request request = new Request.Builder() .url("http://www.baidu.com") .cacheControl(CacheControl.FORCE_NETWORK) .build(); Call call=null; call = mOkHttpClient.newCall(request); final Call finalCall = call; //100毫秒後取消call executor.schedule(new Runnable() { @Override public void run() { finalCall.cancel(); } }, 100, TimeUnit.MILLISECONDS); call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(final Response response) { if (null != response.cacheResponse()) { String str = response.cacheResponse().toString(); Log.i("wangshu", "cache---" + str); } else { try { response.body().string(); } catch (IOException e) { Log.i("wangshu", "IOException"); e.printStackTrace(); } String str = response.networkResponse().toString(); Log.i("wangshu", "network---" + str); } } }); Log.i("wangshu", "是否取消成功"+call.isCanceled()); }
100毫秒後調用call.cancel(),爲了能讓請求耗時,咱們設置每次請求都要請求網絡,運行程序而且不斷的快速點擊發送請求按鈕:
很明顯每次cancel()都失敗了,仍舊成功的訪問了網絡,在cancel()時已經有讀寫操做了因此會報IOException。每隔100毫秒來調用call.cancel()顯然時間間隔太長,咱們設置爲1毫秒並不斷的快速的點擊發送請求按鈕:
沒有請求網絡的log,幾乎每次都取消成功了。
若是每次請求網絡都須要寫重複的代碼絕對是使人頭疼的,網上也有不少對OkHttp封裝的優秀開源項目,功能也很是強大,封裝的意義就在於更加方便的使用,具備拓展性,可是對OkHttp封裝最須要解決的是如下的兩點:
根據以上兩點,咱們也簡單封裝一下,在此只是舉個例子,若是想要使用OkHttp封裝的開源庫,推薦使用OkHttpFinal。
首先呢咱們寫一個抽象類用於請求回調:
public abstract class ResultCallback<T> { public abstract void onError(Request request, Exception e); public abstract void onResponse(Response response); }
接下來封裝OkHttp,並實現了異步GET請求:
public class OkHttpEngine { private static OkHttpEngine mInstance; private OkHttpClient mOkHttpClient; private Handler mHandler; public static OkHttpEngine getInstance() { if (mInstance == null) { synchronized (OkHttpEngine.class) { if (mInstance == null) { mInstance = new OkHttpEngine(); } } } return mInstance; } private OkHttpEngine() { mOkHttpClient = new OkHttpClient(); mOkHttpClient.setConnectTimeout(15, TimeUnit.SECONDS); mOkHttpClient.setWriteTimeout(20, TimeUnit.SECONDS); mOkHttpClient.setReadTimeout(20, TimeUnit.SECONDS); mHandler = new Handler(); } public OkHttpEngine setCache(Context mContext) { File sdcache = mContext.getExternalCacheDir(); int cacheSize = 10 * 1024 * 1024; mOkHttpClient.setCache(new Cache(sdcache.getAbsoluteFile(), cacheSize)); return mInstance; } /* 異步get請求 * @param url * @param callback */ public void getAsynHttp(String url, ResultCallback callback) { final Request request = new Request.Builder() .url(url) .build(); Call call = mOkHttpClient.newCall(request); dealResult(call, callback); } private void dealResult(Call call, final ResultCallback callback) { call.enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { sendFailedCallback(request, e, callback); } @Override public void onResponse(final Response response) throws IOException { sendSuccessCallback(response, callback); } private void sendSuccessCallback(final Response object, final ResultCallback callback) { mHandler.post(new Runnable() { @Override public void run() { if (callback != null) { callback.onResponse(object); } } }); } private void sendFailedCallback(final Request request, final Exception e, final ResultCallback callback) { mHandler.post(new Runnable() { @Override public void run() { if (callback != null) callback.onError(request, e); } }); } }); } }
原理很簡單就是,寫一個雙重檢查模式的單例,不瞭解雙重檢查模式的請查看設計模式之單例模式的七種寫法這篇文章。在開始建立的時候配置好OkHttpClient,在請求網絡的時候用Handler將請求的結果回調給UI線程。
最後調用這個OkHttpEngine的getAsynHttp()方法:
OkHttpEngine.getInstance().getAsynHttp("http://www.baidu.com", new ResultCallback() { @Override public void onError(Request request, Exception e) { } @Override public void onResponse(Response response) { String str = response.networkResponse().toString(); Log.i("wangshu", str); Toast.makeText(getApplicationContext(), "請求成功", Toast.LENGTH_SHORT).show(); } });
使用起來簡單多了,並且請求結果回調是在UI線程的。
Android Studio 配置gradle:
compile 'com.squareup.okhttp3:okhttp:3.2.0' compile 'com.squareup.okio:okio:1.7.0'
添加網絡權限:
<uses-permission android:name="android.permission.INTERNET"/>
與2.x版本並無什麼不一樣,比較鬱悶的是回調仍然不在UI線程。
慣例,請求百度:
private void getAsynHttp() { mOkHttpClient=new OkHttpClient(); Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com"); requestBuilder.method("GET",null);//能夠省略,默認是GET請求 Request request = requestBuilder.build(); Call mcall= mOkHttpClient.newCall(request); mcall.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { if (null != response.cacheResponse()) { String str = response.cacheResponse().toString(); Log.i("wangshu", "cache---" + str); } else { response.body().string(); String str = response.networkResponse().toString(); Log.i("wangshu", "network---" + str); } runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "請求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
OkHttp3異步POST請求和OkHttp2.x有一些差異就是沒有FormEncodingBuilder這個類,替代它的是功能更增強大的FormBody:
private void postAsynHttp() { mOkHttpClient=new OkHttpClient(); RequestBody formBody = new FormBody.Builder() .add("size", "10") .build(); Request request = new Request.Builder() .url("http://api.1-blog.com/biz/bizserver/article/list.do") .post(formBody) .build(); Call call = mOkHttpClient.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { String str = response.body().string(); Log.i("wangshu", str); runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "請求成功", Toast.LENGTH_SHORT).show(); } }); } }); }
上傳文件自己也是一個POST請求,上一篇沒有講,這裏咱們補上。首先定義上傳文件類型:
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
將sdcard根目錄的wangshu.txt文件上傳到服務器上:
private void postAsynFile() { mOkHttpClient=new OkHttpClient(); File file = new File("/sdcard/wangshu.txt"); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))//上傳文件RequestBody.create .build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Log.i("wangshu",response.body().string()); } }); }
固然若是想要改成同步的上傳文件只要調用 mOkHttpClient.newCall(request).execute()就能夠了。
在wangshu.txt文件中有一行字「Android網絡編程(六)OkHttp3用法全解析」咱們運行程序點擊發送文件按鈕,最終請求網絡返回的結果就是咱們txt文件中的內容 :
固然不要忘了添加以下權限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
下載文件一樣在上一篇沒有講到,實現起來比較簡單,在這裏下載一張圖片,咱們獲得Response後將流寫進咱們指定的圖片文件中就能夠了。
private void downAsynFile() { mOkHttpClient = new OkHttpClient(); String url = "http://img.my.csdn.net/uploads/201603/26/1458988468_5804.jpg"; Request request = new Request.Builder().url(url).build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) { //獲得Response後將流寫進咱們指定的圖片文件 InputStream inputStream = response.body().byteStream(); FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(new File("/sdcard/wangshu.jpg")); byte[] buffer = new byte[2048]; int len = 0; while ((len = inputStream.read(buffer)) != -1) { fileOutputStream.write(buffer, 0, len); } fileOutputStream.flush(); } catch (IOException e) { Log.i("wangshu", "IOException"); e.printStackTrace(); } Log.d("wangshu", "文件下載成功"); } }); }
關鍵類MultipartBody
這種場景很經常使用,咱們有時會上傳文件同時還須要傳其餘類型的字段,OkHttp3實現起來很簡單,須要注意的是沒有服務器接收我這個Multipart文件,因此這裏只是舉個例子,具體的應用還要結合實際工做中對應的服務器。
首先定義上傳文件類型:
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private void sendMultipart(){ mOkHttpClient = new OkHttpClient(); //關鍵類MultipartBody RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "wangshu") .addFormDataPart("image", "wangshu.jpg", RequestBody.create(MEDIA_TYPE_PNG, new File("/sdcard/wangshu.jpg"))) .build(); Request request = new Request.Builder() .header("Authorization", "Client-ID " + "...") .url("https://api.imgur.com/3/image") .post(requestBody) .build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Log.i("wangshu", response.body().string()); } }); }
和OkHttp2.x有區別的是不能經過OkHttpClient直接設置超時時間和緩存了,而是經過OkHttpClient.Builder
來設置,經過builder配置好OkHttpClient後用builder.build()來返回OkHttpClient,因此咱們一般不會調用new OkHttpClient()來獲得OkHttpClient,而是經過builder.build():
File sdcache = getExternalCacheDir();
int cacheSize = 10 * 1024 * 1024; OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .cache(new Cache(sdcache.getAbsoluteFile(), cacheSize)); OkHttpClient mOkHttpClient=builder.build();
取消請求仍舊能夠調用call.cancel(),這個沒有變化,不明白的能夠查看上一篇文章Android網絡編程(五)OkHttp2.x用法全解析,這裏就不贅述了,封裝上一篇也講過仍舊推薦OkHttpFinal,它目前是基於OkHttp3來進行封裝的。
當咱們要請求網絡的時候咱們須要用OkHttpClient.newCall(request)進行execute或者enqueue操做,當咱們調用newCall時:
@Override public Call newCall(Request request) { return new RealCall(this, request); }
實際返回的是一個RealCall類,咱們調用enqueue異步請求網絡其實是調用了RealCall的enqueue方法:
void enqueue(Callback responseCallback, boolean forWebSocket) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket)); }
能夠看到最終的請求是dispatcher來完成的。
Dispatcher主要用於控制併發的請求,它主要維護瞭如下變量:
/* 最大併發請求數*/ private int maxRequests = 64; /* 每一個主機最大請求數*/ private int maxRequestsPerHost = 5; /* 消費者線程池 */ private ExecutorService executorService; /* 將要運行的異步請求隊列 */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /* 正在運行的異步請求隊列 */ private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); /* 正在運行的同步請求隊列 */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) { this.executorService = executorService; } public Dispatcher() { } public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }
Dispatcher有兩個構造函數,可使用本身設定線程池,若是沒有設定線程池則會在請求網絡前本身建立線程池,這個線程池相似於CachedThreadPool比較適合執行大量的耗時比較少的任務。不瞭解線程池的同窗能夠查看Android多線程(一)線程池這篇文章。其中用到了SynchronousQueue,不瞭解它的同窗能夠查看Java併發編程(六)阻塞隊列這篇文章。
synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); } }
當正在運行的異步請求隊列中的數量小於64而且正在運行的請求主機數小於5時則把請求加載到runningAsyncCalls中並在線程池中執行,不然就再入到readyAsyncCalls中進行緩存等待。
線程池中傳進來的參數就是AsyncCall它是RealCall的內部類,內部也實現了execute方法:
@Override protected void execute() { boolean signalledCallback = false; try { Response response = getResponseWithInterceptorChain(forWebSocket); if (canceled) { signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } } catch (IOException e) { if (signalledCallback) { // Do not signal the callback twice! logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e); } else { responseCallback.onFailure(RealCall.this, e); } } finally { client.dispatcher().finished(this); } }
首先咱們來看看最後一行, 不管這個請求的結果如何都會執行client.dispatcher().finished(this);
synchronized void finished(AsyncCall call) { if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!"); promoteCalls(); }
finished方法將這次請求從runningAsyncCalls移除後還執行了promoteCalls方法:
private void promoteCalls() { if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity. if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote. for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { AsyncCall call = i.next(); if (runningCallsForHost(call) < maxRequestsPerHost) { i.remove(); runningAsyncCalls.add(call); executorService().execute(call); } if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity. } }
能夠看到最關鍵的點就是會從readyAsyncCalls取出下一個請求,並加入runningAsyncCalls中並交由線程池處理。好了讓咱們再回到上面的AsyncCall的execute方法,咱們會發getResponseWithInterceptorChain方法返回了Response,很明顯這是在請求網絡。
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket); return chain.proceed(originalRequest); }
getResponseWithInterceptorChain方法,建立了ApplicationInterceptorChain,它是一個攔截器鏈,這個類也是RealCall的內部類,接下來執行了它的proceed方法:
@Override
public Response proceed(Request request) throws IOException { // If there's another interceptor in the chain, call that. if (index < client.interceptors().size()) { Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket); //從攔截器列表取出攔截器 Interceptor interceptor = client.interceptors().get(index); Response interceptedResponse = interceptor.intercept(chain); if (interceptedResponse == null) { throw new NullPointerException("application interceptor " + interceptor + " returned null"); } return interceptedResponse; } // No more interceptors. Do HTTP. return getResponse(request, forWebSocket); }
proceed方法每次從攔截器列表中取出攔截器,當存在多個攔截器時都會在第七行阻塞,並等待下一個攔截器的調用返回。下面分別以 攔截器鏈中有1個、2個攔截器的場景加以模擬:
攔截器主要用來觀察,修改以及可能短路的請求輸出和響應的回來。一般狀況下攔截器用來添加,移除或者轉換請求或者響應的頭部信息。好比將域名替換爲ip地址,將請求頭中添加host屬性,也能夠添加咱們應用中的一些公共參數,好比設備id、版本號等等。 不瞭解攔截器的能夠查看Okhttp-wiki 之 Interceptors 攔截器這篇文章。
回到代碼上來,咱們看最後一行 return getResponse(request, forWebSocket),若是沒有更多的攔截器的話,就會執行網絡請求,來看看getResponse方法作了些什麼(RealCall.java):
Response getResponse(Request request, boolean forWebSocket) throws IOException { ...省略 // Create the initial HTTP engine. Retries and redirects need new engine for each attempt. engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null); int followUpCount = 0; while (true) { if (canceled) { engine.releaseStreamAllocation(); throw new IOException("Canceled"); } boolean releaseConnection = true; try { engine.sendRequest(); engine.readResponse(); releaseConnection = false; } catch (RequestException e) { // The attempt to interpret the request failed. Give up. throw e.getCause(); } catch (RouteException e) { // The attempt to connect via a route failed. The request will not have been sent. ...省略 } }
getResponse方法比較長我省略了一些代碼,能夠看到建立了HttpEngine類而且調用HttpEngine的sendRequest方法和readResponse方法。
咱們先來看看sendRequest方法:
public void sendRequest() throws RequestException, RouteException, IOException { if (cacheStrategy != null) return; // Already sent. if (httpStream != null) throw new IllegalStateException(); //請求頭部添加 Request request = networkRequest(userRequest); //獲取client中的Cache,同時Cache在初始化的時候會去讀取緩存目錄中關於曾經請求過的全部信息。 InternalCache responseCache = Internal.instance.internalCache(client); //cacheCandidate爲上次與服務器交互緩存的Response Response cacheCandidate = responseCache != null ? responseCache.get(request) : null; long now = System.currentTimeMillis(); //建立CacheStrategy.Factory對象,進行緩存配置 cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get(); //網絡請求 networkRequest = cacheStrategy.networkRequest; //緩存的響應 cacheResponse = cacheStrategy.cacheResponse; if (responseCache != null) { //記錄當前請求是網絡發起仍是緩存發起 responseCache.trackResponse(cacheStrategy); } if (cacheCandidate != null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. } //不進行網絡請求而且緩存不存在或者過時則返回504錯誤 if (networkRequest == null && cacheResponse == null) { userResponse = new Response.Builder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(EMPTY_BODY) .build(); return; } // 不進行網絡請求,並且緩存可使用,直接返回緩存 if (networkRequest == null) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .build(); userResponse = unzip(userResponse); return; } //須要訪問網絡時 boolean success = false; try { httpStream = connect(); httpStream.setHttpEngine(this); if (writeRequestHeadersEagerly()) { long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // Buffer a request body of a known length. httpStream.writeRequestHeaders(networkRequest); requestBodyOut = new RetryableSink((int) contentLength); } else { // Buffer a request body of an unknown length. Don't write request headers until the // entire body is ready; otherwise we can't set the Content-Length header correctly. requestBodyOut = new RetryableSink(); } } else { httpStream.writeRequestHeaders(networkRequest); requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength); } } success = true; } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (!success && cacheCandidate != null) { closeQuietly(cacheCandidate.body()); } } }
上面的代碼顯然是在發送請求,可是最主要的是作了緩存的策略。cacheCandidate是上次與服務器交互緩存的Response,這裏的緩存都是基於Map,key是請求中url的md5,value是在文件中查詢到的緩存,頁面置換基於LRU算法,咱們如今只須要知道它是一個能夠讀取緩存Header的Response便可。根據cacheStrategy的處理獲得了networkRequest和cacheResponse這兩個值,根據這兩個值的數據是否爲null來進行進一步的處理,當networkRequest和cacheResponse都爲null的狀況也就是不進行網絡請求而且緩存不存在或者過時,這時候則返回504錯誤;當networkRequest 爲null時也就是不進行網絡請求,並且緩存可使用時則直接返回緩存;其餘的狀況則請求網絡。
接下來咱們查看readResponse方法:
public void readResponse() throws IOException { ...省略 else{ //讀取網絡響應 networkResponse = readNetworkResponse(); } //將響應頭部存入Cookie中 receiveHeaders(networkResponse.headers()); // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { //檢查緩存是否可用,若是可用。那麼就用當前緩存的Response,關閉網絡鏈接,釋放鏈接。 if (validate(cacheResponse, networkResponse)) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .headers(combine(cacheResponse.headers(), networkResponse.headers())) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); releaseStreamAllocation(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). InternalCache responseCache = Internal.instance.internalCache(client); responseCache.trackConditionalCacheHit(); // 更新緩存 responseCache.update(cacheResponse, stripBody(userResponse)); userResponse = unzip(userResponse); return; } else { closeQuietly(cacheResponse.body()); } } userResponse = networkResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (hasBody(userResponse)) { maybeCache(); userResponse = unzip(cacheWritingResponse(storeRequest, userResponse)); } }
這個方法發起刷新請求頭部和請求體,解析HTTP響應頭部。若是有緩存而且可用則用緩存的數據並更新緩存,不然就用網絡請求返回的數據。
咱們再來看看validate(cacheResponse, networkResponse)方法是如何判斷緩存是否可用的:
private static boolean validate(Response cached, Response network) { //若是服務器返回304則緩存有效 if (network.code() == HTTP_NOT_MODIFIED) { return true; } //經過緩存和網絡請求響應中的Last-Modified來計算是不是最新數據,若是是則緩存有效 Date lastModified = cached.headers().getDate("Last-Modified"); if (lastModified != null) { Date networkLastModified = network.headers().getDate("Last-Modified"); if (networkLastModified != null && networkLastModified.getTime() < lastModified.getTime()) { return true; } } return false; }
如緩存果過時或者強制放棄緩存,在此狀況下,緩存策略所有交給服務器判斷,客戶端只用發送條件get請求便可,若是緩存是有效的,則返回304 Not Modifiled,不然直接返回body。條件get請求有兩種方式一種是Last-Modified-Date,一種是 ETag。這裏採用了Last-Modified-Date,經過緩存和網絡請求響應中的Last-Modified來計算是不是最新數據,若是是則緩存有效。
最後咱們再回到RealCall的getResponse方法:
Response getResponse(Request request, boolean forWebSocket) throws IOException { ...省略 boolean releaseConnection = true; try { engine.sendRequest(); engine.readResponse(); releaseConnection = false; } catch (RequestException e) { // The attempt to interpret the request failed. Give up. throw e.getCause(); } catch (RouteException e) { // The attempt to connect via a route failed. The request will not have been sent. HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null); if (retryEngine != null) { releaseConnection = false; engine = retryEngine; continue; } // Give up; recovery is not possible. throw e.getLastConnectException(); } catch (IOException e) { // An attempt to communicate with a server failed. The request may have been sent. HttpEngine retryEngine = engine.recover(e, null); if (retryEngine != null) { releaseConnection = false; engine = retryEngine; continue; } // Give up; recovery is not possible. throw e; } finally { // We're throwing an unchecked exception. Release any resources. if (releaseConnection) { StreamAllocation streamAllocation = engine.close(); streamAllocation.release(); } } ...省略 engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null, response); } }
查看代碼第11行和21行當發生IOException或者RouteException時會執行HttpEngine的recover方法:
public HttpEngine recover(IOException e, Sink requestBodyOut) { if (!streamAllocation.recover(e, requestBodyOut)) { return null; } if (!client.retryOnConnectionFailure()) { return null; } StreamAllocation streamAllocation = close(); // For failure recovery, use the same route selector with a new connection. return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody, forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse); }
最後一行能夠看到就是從新建立了HttpEngine並返回,用來完成重連。
到這裏OkHttp請求網絡的流程基本上講完了,下面是關於OKHttp的請求流程圖:
參考資料:
http://www.jianshu.com/p/aad5aacd79bf
http://www.jianshu.com/p/64e256c1dbbf
http://www.cnblogs.com/LuLei1990/p/5534791.html
http://frodoking.github.io/2015/03/12/android-okhttp/
固然大量的鏈接每次鏈接關閉都要三次握手四次分手的很顯然會形成性能低下,所以http有一種叫作keepalive connections的機制,它能夠在傳輸數據後仍然保持鏈接,當客戶端須要再次獲取數據時,直接使用剛剛空閒下來的鏈接而不須要再次握手。
Okhttp支持5個併發KeepAlive,默認鏈路生命爲5分鐘(鏈路空閒後,保持存活的時間)。
在okhttp中,在高層代碼的調用中,使用了相似於引用計數的方式跟蹤Socket流的調用,這裏的計數對象是StreamAllocation,它被反覆執行aquire與release操做,這兩個函數實際上是在改變RealConnection中的List<Reference<StreamAllocation>>
的大小。(StreamAllocation.java)
public void acquire(RealConnection connection) { connection.allocations.add(new WeakReference<>(this)); }
private void release(RealConnection connection) { for (int i = 0, size = connection.allocations.size(); i < size; i++) { Reference<StreamAllocation> reference = connection.allocations.get(i); if (reference.get() == this) { connection.allocations.remove(i); return; } } throw new IllegalStateException(); }
RealConnection是socket物理鏈接的包裝,它裏面維護了List<Reference<StreamAllocation>>
的引用。List中StreamAllocation的數量也就是socket被引用的計數,若是計數爲0的話,說明此鏈接沒有被使用就是空閒的,須要經過下文的算法實現回收;若是計數不爲0,則表示上層代碼仍然引用,就不須要關閉鏈接。
鏈接池的類位於okhttp3.ConnectionPool:
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */, Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true)); /** The maximum number of idle connections for each address. */ //空閒的socket最大鏈接數 private final int maxIdleConnections; //socket的keepAlive時間 private final long keepAliveDurationNs; // 雙向隊列 private final Deque<RealConnection> connections = new ArrayDeque<>(); final RouteDatabase routeDatabase = new RouteDatabase(); boolean cleanupRunning;
主要的變量有必要說明一下:
Deque<RealConnection>
,雙向隊列,雙端隊列同時具備隊列和棧性質,常常在緩存中被使用,裏面維護了RealConnection也就是socket物理鏈接的包裝。public ConnectionPool() { //默認空閒的socket最大鏈接數爲5個,socket的keepAlive時間爲5秒 this(5, 5, TimeUnit.MINUTES); } public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) { this.maxIdleConnections = maxIdleConnections; this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration); // Put a floor on the keep alive duration, otherwise cleanup will spin loop. if (keepAliveDuration <= 0) { throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration); } }
經過構造函數能夠看出ConnectionPool默認的空閒的socket最大鏈接數爲5個,socket的keepAlive時間爲5秒。
ConnectionPool實例化是在OkHttpClient實例化時進行的:
public OkHttpClient() { this(new Builder()); }
在OkHttpClient的構造函數中調用了new Builder():
public Builder() {
dispatcher = new Dispatcher();
...省略 connectionPool = new ConnectionPool(); ...省略 }
ConnectionPool提供對Deque<RealConnection>
進行操做的方法分別爲put、get、connectionBecameIdle和evictAll幾個操做。分別對應放入鏈接、獲取鏈接、移除鏈接和移除全部鏈接操做,這裏咱們舉例put和get操做。
put操做
void put(RealConnection connection) { assert (Thread.holdsLock(this)); if (!cleanupRunning) { cleanupRunning = true; executor.execute(cleanupRunnable); } connections.add(connection); }
在添加到Deque<RealConnection>
以前首先要清理空閒的線程,這個後面會講到。
get操做
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { if (connection.allocations.size() < connection.allocationLimit && address.equals(connection.route().address) && !connection.noNewStreams) { streamAllocation.acquire(connection); return connection; } } return null; }
遍歷connections緩存列表,當某個鏈接計數的次數小於限制的大小而且request的地址和緩存列表中此鏈接的地址徹底匹配。則直接複用緩存列表中的connection做爲request的鏈接。
okhttp是根據StreamAllocation引用計數是否爲0來實現自動回收鏈接的。咱們在put操做前首先要調用executor.execute(cleanupRunnable)
來清理閒置的線程。咱們來看看cleanupRunnable到底作了什麼:
private final Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { try { ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } };
線程不斷的調用cleanup來進行清理,並返回下次須要清理的間隔時間,而後調用wait進行等待以釋放鎖與時間片,當等待時間到了後,再次進行清理,並返回下次要清理的間隔時間,如此循環下去,接下來看看cleanup方法:
long cleanup(long now) { int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. synchronized (this) { //遍歷鏈接 for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); //查詢此鏈接的StreamAllocation的引用數量,若是大於0則inUseConnectionCount數量加1,不然idleConnectionCount加1 if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //若是空閒鏈接keepAlive時間超過5分鐘,或者空閒鏈接數超過5個,則從Deque中移除此鏈接 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // We've found a connection to evict. Remove it from the list, then close it below (outside // of the synchronized block). connections.remove(longestIdleConnection); //若是空閒鏈接大於0,則返回此鏈接即將到期的時間 } else if (idleConnectionCount > 0) { // A connection will be ready to evict soon. return keepAliveDurationNs - longestIdleDurationNs; //若是沒有空閒鏈接,而且活躍鏈接大於0則返回5分鐘 } else if (inUseConnectionCount > 0) { // All connections are in use. It'll be at least the keep alive duration 'til we run again. return keepAliveDurationNs; } else { //若是沒有任何鏈接則跳出循環 cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }
cleanup所作的簡單總結就是根據鏈接中的引用計數來計算空閒鏈接數和活躍鏈接數,而後標記出空閒的鏈接,若是空閒鏈接keepAlive時間超過5分鐘,或者空閒鏈接數超過5個,則從Deque中移除此鏈接。接下來根據空閒鏈接或者活躍鏈接來返回下次須要清理的時間數:若是空閒鏈接大於0則返回此鏈接即將到期的時間,若是都是活躍鏈接而且大於0則返回默認的keepAlive時間5分鐘,若是沒有任何鏈接則跳出循環並返回-1。在上述代碼中的第13行,經過pruneAndGetAllocationCount方法來判斷鏈接是否閒置的,若是pruneAndGetAllocationCount方法返回值大於0則是空閒鏈接,不然就是活躍鏈接,讓咱們來看看pruneAndGetAllocationCount方法:
private int pruneAndGetAllocationCount(RealConnection connection, long now) { List<Reference<StreamAllocation>> references = connection.allocations; //遍歷弱引用列表 for (int i = 0; i < references.size(); ) { Reference<StreamAllocation> reference = references.get(i); //若StreamAllocation被使用則接着循環 if (reference.get() != null) { i++; continue; } // We've discovered a leaked allocation. This is an application bug. Internal.logger.warning("A connection to " + connection.route().address().url() + " was leaked. Did you forget to close a response body?"); //若StreamAllocation未被使用則移除引用 references.remove(i); connection.noNewStreams = true; // If this was the last allocation, the connection is eligible for immediate eviction. //若是列表爲空則說明此鏈接沒有被引用了,則返回0,表示此鏈接是空閒鏈接 if (references.isEmpty()) { connection.idleAtNanos = now - keepAliveDurationNs; return 0; } } //不然返回非0的數,表示此鏈接是活躍鏈接 return references.size(); }
pruneAndGetAllocationCount方法首先遍歷傳進來的RealConnection的StreamAllocation列表,若是StreamAllocation被使用則接着遍歷下一個StreamAllocation,若是StreamAllocation未被使用則從列表中移除。若是列表爲空則說明此鏈接沒有引用了,則返回0,表示此鏈接是空閒鏈接,不然就返回非0的數表示此鏈接是活躍鏈接。
能夠看出鏈接池複用的核心就是用Deque<RealConnection>
來存儲鏈接,經過put、get、connectionBecameIdle和evictAll幾個操做來對Deque進行操做,另外經過判斷鏈接中的計數對象StreamAllocation來進行自動回收鏈接。
參考資料
okhttp3源碼
簡析TCP的三次握手與四次分手
TCP三次握手過程
短鏈接、長鏈接與keep-alive
OkHttp3源碼分析[複用鏈接池]
okhttp鏈接池複用機制
Retrofit是Square公司開發的一款針對Android網絡請求的框架,Retrofit2**底層基於OkHttp**實現的,而OkHttp如今已經獲得Google官方承認。
老生長談,先配置build.gradle:
dependencies {
...
compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:converter-scalars:2.1.0'//ConverterFactory的String依賴包 }
固然別忘了在manifest加入訪問網絡的權限:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
此次咱們訪問的網站產生了變化,咱們用淘寶IP地址庫,裏面有訪問接口的說明:
1. 請求接口(GET):
/service/getIpInfo.php?ip=[ip地址字串]
2. 響應信息:
(json格式的)國家 、省(自治區或直轄市)、市(縣)、運營商
3. 返回數據格式:
{ "code": 0, "data": { "ip": "210.75.225.254", "country": "\u4e2d\u56fd", "area": "\u534e\u5317", "region": "\u5317\u4eac\u5e02", "city": "\u5317\u4eac\u5e02", "county": "", "isp": "\u7535\u4fe1", "country_id": "86", "area_id": "100000", "region_id": "110000", "city_id": "110000", "county_id": "-1", "isp_id": "100017" } }
其中code的值的含義爲,0:成功,1:失敗。
咱們能夠用JSON字符串轉換成Java實體類(POJO)這個網站將Json轉爲實體類,通過修改的實體類以下:
IpModel.java:
public class IpModel { private int code; private IpData data; public void setCode(int code) { this.code = code; } public int getCode() { return this.code; } public void setData(IpData data) { this.data = data; } public IpData getData() { return this.data; } }
IpData.java:
public class IpData { private String country; private String country_id; private String area; private String area_id; private String region; private String region_id; private String city; private String city_id; private String county; private String county_id; private String isp; private String isp_id; private String ip; public void setCountry(String country) { this.country = country; } public String getCountry() { return this.country; } public void setCountry_id(String country_id) { this.country_id = country_id; } ... }
上文已知,請求接口(GET方式)爲:/service/getIpInfo.php?ip=[ip地址字串]
public interface IpService{ @GET("/service/getIpInfo.php") Call<IpModel> getIpMsg(@Query("ip")String ip); }
Retrofit提供的請求方式註解有@GET和@POST等,分別表明GET請求和POST請求,咱們在這裏訪問的界面是「getIpInfo.php」。
參數註解有@PATH和@Query等,@Query就是咱們的請求的鍵值對的設置,在這裏@Query**(「ip」)表明鍵,**String ip則表明值。
String url = "http://ip.taobao.com"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) //增長返回值爲String的支持 .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build();
這裏的baseUrl加上以前@GET(「/service/getIpInfo.php」)定義的參數造成完整的請求地址http://ip.taobao.com/service/getIpInfo.php
; addConverterFactory
用於指定返回的參數數據類型,這裏咱們支持String和Gson類型。
IpService ipService = retrofit.create(IpService.class); Call<IpModel>call=ipService.getIpMsg(ip);
用retrofit建立咱們以前定義的IpService接口對象,並調用該接口定義的getIpMsg方法獲得Call對象。
call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Log.i("wangshu","country"+country); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
這裏是異步請求網絡,回調的Callback是運行在主線程的。獲得返回的Response後將返回數據的country字段用Toast顯示出來。
若是想同步請求網絡請使用 call.execute()。
若是想中斷網絡請求則可使用 call.cancel()。
完整的代碼以下:
public class MainActivity extends AppCompatActivity { private Button bt_request; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bt_request = (Button) findViewById(R.id.bt_request); bt_request.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { getIpInformation("59.108.54.37"); } }); } private void getIpInformation(String ip) { String url = "http://ip.taobao.com/service/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) //增長返回值爲String的支持 .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); IpService ipService = retrofit.create(IpService.class); Call<IpModel>call=ipService.getIpMsg(ip); call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Log.i("wangshu","country"+country); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } }); }
上文講了Retrofit訪問網絡的基本方法,接下來咱們來了解下Retrofit經常使用的請求參數。
請求方法除了上文講到的@GET,還有@POST、@PUT、@DELETE、@HEAD、@OPTIONS、@PATCH、@HTTP。
其中@HTTP用來替換以上7個,其餘的分別對應着不一樣的請求方法。
前面的例子就用了Query用來查詢參數。
public interface IpService{ @GET("getIpInfo.php") Call<IpModel> getIpMsg(@Query("ip")String ip); }
若是Query**參數比較多,那麼能夠經過@QueryMap方式將全部的參數集成在一個Map統一傳遞**。
public interface BlueService { @GET("book/search") Call<BookSearchResponse> getSearchBooks(@QueryMap Map<String, String> options); }
@Path用來替換路徑。
public interface ApiStores { @GET("adat/sk/{cityId}.html") Call<ResponseBody> getWeather(@Path("cityId") String cityId); }
@Body與@POST註解一塊兒使用,提供查詢主體內容,其中ApiInfo是一個bean類。
public interface ApiStores { @POST("client/shipper/getCarType") Call<ResponseBody> getCarType(@Body ApiInfo apiInfo); }
interface SomeService {
@GET("some/endpoint") @Headers("Accept-Encoding: application/json") Call<ResponseBody> getCarType(); }
@Headers用來添加頭部信息,上面用的是固定頭部,也能夠採用動態頭部:
interface SomeService {
@GET("some/endpoint") Call<SomeResponse> someEndpoint(@Header("Location") String location); }
@Multipart用來上傳文件
public interface FileUploadService { @Multipart @POST("upload") Call<ResponseBody> upload(@Part("description") RequestBody description, @Part MultipartBody.Part file); }
github源碼下載
參考資料
Retrofit 2.0文件上傳
RxJava 與 Retrofit 結合的最佳實踐
Retrofit2使用初探
android 介紹Retrofit的簡單使用
Retrofit框架使用筆記
Retrofit 解析 JSON 數據
用 Retrofit 2 簡化 HTTP 請求
Android Retrofit 2.0使用
Retrofit提供了不少的請求參數註解,使得請求網路時更加便捷。在這裏咱們仍舊訪問淘寶IP地址庫。
其中,@Path用來動態的配置URL地址。請求網絡接口代碼以下所示。
public interface IpServiceForPath { @GET("{path}/getIpInfo.php?ip=59.108.54.37") Call<IpModel> getIpMsg(@Path("path") String path); }
在GET註解中包含了{path},它對應着@Path註解中的」path」,而用來替換{path}的正是須要傳入的」String path」的值。
接下來請求網絡的代碼以下所示。
String url = "http://ip.taobao.com/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build(); IpServiceForPath ipService = retrofit.create(IpServiceForPath.class); Call<IpModel>call=ipService.getIpMsg("service");//1 call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
在註釋1處,傳入」service」來替換 @GET註解中的{path}的值。
在上一篇中咱們用@Query來動態的替換ip地址爲了能更方便的獲得該ip所對應的地理信息:
public interface IpServiceForQuery{ @GET("getIpInfo.php") Call<IpModel> getIpMsg(@Query("ip")String ip); }
可是在網絡請求中通常爲了更精確的查找到咱們所須要的數據,須要傳入不少的查詢參數,若是用@Query會比較麻煩,這時咱們能夠採用@QueryMap,將全部的參數集成在一個Map統一傳遞:
public interface IpServiceForQueryMap { @GET("getIpInfo.php") Call<IpModel> getIpMsg(@QueryMap Map<String, String> options); }
傳輸數據類型爲鍵值對,這是咱們最經常使用的POST請求數據類型,淘寶ip庫支持數據類型爲鍵值對的POST請求:
public interface IpServiceForPost { @FormUrlEncoded @POST("getIpInfo.php") Call<IpModel> getIpMsg(@Field("ip") String first); }
首先用到@FormUrlEncoded
註解來標明這是一個表單請求,
而後在getIpMsg方法中使用@Field
註解來標示所對應的String類型數據的鍵,從而組成一組鍵值對進行傳遞。
接下來請求網絡的代碼以下所示。
String url = "http://ip.taobao.com/service/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build(); IpServiceForPost ipService = retrofit.create(IpServiceForPost.class); Call<IpModel>call=ipService.getIpMsg("59.108.54.37"); call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
咱們也能夠用POST方式將Json字符串做爲請求體發送到服務器,請求網絡接口代碼爲:
public interface IpServiceForPostBody { @POST("getIpInfo.php") Call<IpModel> getIpMsg(@Body Ip ip); }
用@Body
這個註解標識參數對象便可,retrofit會將Ip對象轉換爲字符串。
public class Ip { private String ip; public Ip(String ip) { this.ip = ip; } }
請求網絡的代碼基本上都是一致的:
String url = "http://ip.taobao.com/service/"; Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build(); IpServiceForPostBody ipService = retrofit.create(IpServiceForPostBody.class); Call<IpModel>call=ipService.getIpMsg(new Ip(ip)); call.enqueue(new Callback<IpModel>() { @Override public void onResponse(Call<IpModel> call, Response<IpModel> response) { String country= response.body().getData().getCountry(); Log.i("wangshu","country"+country); Toast.makeText(getApplicationContext(),country,Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<IpModel> call, Throwable t) { } });
運行程序用Fiddler抓包,以下圖所示。
能夠看到請求數據是一個Json字符串,由於淘寶ip庫並不支持此類型因此不會返回咱們須要的地理信息數據。
public interface UploadFileForPart { @Multipart @POST("user/photo") Call<User> updateUser(@Part MultipartBody.Part photo, @Part("description") RequestBody description); }
@Multipart
註解表示容許多個@Part,updateUser方法第一個參數是準備上傳的圖片文件,使用了MultipartBody.Part類型,另外一個參數是RequestBody類型,它用來傳遞簡單的鍵值對。請求網絡代碼以下所示。
...
File file = new File(Environment.getExternalStorageDirectory(), "wangshu.png"); RequestBody photoRequestBody = RequestBody.create(MediaType.parse("image/png"), file); MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "wangshu.png", photoRequestBody); UploadFileForPart uploadFile = retrofit.create(UploadFileForPart.class); Call<User> call = uploadFile.updateUser(photo, RequestBody.create(null, "wangshu")); ...
@Multipart @POST("user/photo") Call<User> updateUser(@PartMap Map<String, RequestBody> photos, @Part("description") RequestBody description);
和單文件上傳是相似的,只是使用Map封裝了上傳的文件,並用@PartMap註解來標示起來。其餘的都同樣,這裏就不贅述了。
Http請求中,爲了防止攻擊或是過濾掉不安全的訪問或是添加特殊加密的訪問等等,用來減輕服務器的壓力和保證請求的安全,一般都會在消息報頭中攜帶一些特殊的消息頭處理。
Retrofit也提供了@Header來添加消息報頭。
添加消息報頭有兩種方式,一種是靜態的,另外一種是動態的,
先來看靜態方式,以下所示
interface SomeService {
@GET("some/endpoint") @Headers("Accept-Encoding: application/json") Call<ResponseBody> getCarType(); }
使用@Headers註解添加消息報頭,若是想要添加多個消息報頭,則可使用{}包含起來:
interface SomeService {
@GET("some/endpoint") @Headers({ "Accept-Encoding: application/json", "User-Agent: MoonRetrofit" }) Call<ResponseBody> getCarType(); }
動態方式添加消息報頭以下所示。
interface SomeService {
@GET("some/endpoint") Call<ResponseBody> getCarType( @Header("Location") String location); }
使用@Header註解,能夠經過調用getCarType方法來動態的添加消息報頭。
前言
最近博客的產出確實不多,由於博主我正在寫一本Android進階書籍,兩頭很難兼顧,可是每月也得至少發一篇博客。上一篇咱們介紹了Retrofit的使用方法,這一篇咱們照例來學習Retrofit的源碼。
當咱們使用Retrofit請求網絡時,首先要寫請求接口:
public interface IpService { @GET("getIpInfo.php?ip=59.108.54.37") Call<IpModel> getIpMsg();
接着咱們經過調用以下代碼來建立Retrofit:
Retrofit retrofit = new Retrofit.Builder() .baseUrl(url) .addConverterFactory(GsonConverterFactory.create()) .build();
Retrofit 是經過建造者模式構建出來的,接下來查看Builder方法作了什麼:
public Builder() { this(Platform.get()); }
很簡短,查看Platform的get方法,以下所示。
private static final Platform PLATFORM = findPlatform(); static Platform get() { return PLATFORM; } private static Platform findPlatform() { try { Class.forName("android.os.Build"); if (Build.VERSION.SDK_INT != 0) { return new Android(); } } catch (ClassNotFoundException ignored) { } try { Class.forName("java.util.Optional"); return new Java8(); } catch (ClassNotFoundException ignored) { } try { Class.forName("org.robovm.apple.foundation.NSObject"); return new IOS(); } catch (ClassNotFoundException ignored) { } return new Platform(); }
Platform的get方法最終調用的是findPlatform方法,根據不一樣的運行平臺來提供不一樣的線程池。接下來查看build方法,代碼以下所示。
public Retrofit build() { if (baseUrl == null) {//1 throw new IllegalStateException("Base URL required."); } okhttp3.Call.Factory callFactory = this.callFactory;//2 if (callFactory == null) { callFactory = new OkHttpClient();//3 } Executor callbackExecutor = this.callbackExecutor; if (callbackExecutor == null) { callbackExecutor = platform.defaultCallbackExecutor();//4 } List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);//5 adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor)); List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);//6 return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories, callbackExecutor, validateEagerly); }
從註釋1處能夠看出baseUrl 是必須指定的。註釋2處callFactory默認爲this.callFactory,this.callFactory就是咱們在構建Retrofit時調用callFactory方法所傳進來的,以下所示。
public Builder callFactory(okhttp3.Call.Factory factory) { this.callFactory = checkNotNull(factory, "factory == null"); return this; }
所以,若是須要對OkHttpClient進行設置,則能夠構建OkHttpClient對象,而後調用callFactory方法將設置好的OkHttpClient傳進去。註釋3處,若是沒有設置callFactory則直接建立OkHttpClient。註釋4的callbackExecutor用來將回調傳遞到UI線程。註釋5的adapterFactories主要用於存儲對Call進行轉化的對象,後面在Call的建立過程會再次提到它。註釋6處的converterFactories主要用於存儲轉化數據對象,後面也會說起到。此前在例子中調用的addConverterFactory(GsonConverterFactory.create()),就是設置返回的數據支持轉換爲Gson對象。最終會返回配置好的Retrofit類。
緊接着咱們建立Retrofit實例並調用以下代碼來生成接口的動態代理對象:
IpService ipService = retrofit.create(IpService.class);
接下來看Retrofit的create方法作了什麼,代碼以下所示。
public <T> T create(final Class<T> service) { Utils.validateServiceInterface(service); if (validateEagerly) { eagerlyValidateMethods(service); } return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { private final Platform platform = Platform.get(); @Override public Object invoke(Object proxy, Method method, Object... args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } if (platform.isDefaultMethod(method)) { return platform.invokeDefaultMethod(method, service, proxy, args); } ServiceMethod serviceMethod = loadServiceMethod(method);//1 OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } }); }
能夠看到create方法返回了一個Proxy.newProxyInstance動態代理對象,當咱們調用IpService的getIpMsg方法最終會調用InvocationHandler的invoke 方法,它有3個參數,第一個是代理對象,第二個是調用的方法,第三個是方法的參數。註釋1處的loadServiceMethod(method)中的method就是咱們定義的getIpMsg方法。接下來查看loadServiceMethod方法裏作了什麼:
private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>(); ServiceMethod loadServiceMethod(Method method) { ServiceMethod result; synchronized (serviceMethodCache) { result = serviceMethodCache.get(method); if (result == null) { result = new ServiceMethod.Builder(this, method).build(); serviceMethodCache.put(method, result); } } return result; }
首先會從serviceMethodCache查詢傳入的方法是否有緩存,若是有就用緩存的ServiceMethod,若是沒有就建立一個,並加入serviceMethodCache緩存起來。接下來看ServiceMethod是如何構建的,代碼以下所示。
public ServiceMethod build() {
callAdapter = createCallAdapter();//1 responseType = callAdapter.responseType();//2 if (responseType == Response.class || responseType == okhttp3.Response.class) { throw methodError("'" + Utils.getRawType(responseType).getName() + "' is not a valid response body type. Did you mean ResponseBody?"); } responseConverter = createResponseConverter();//3 for (Annotation annotation : methodAnnotations) { parseMethodAnnotation(annotation);//4 } ... int parameterCount = parameterAnnotationsArray.length; parameterHandlers = new ParameterHandler<?>[parameterCount]; for (int p = 0; p < parameterCount; p++) { Type parameterType = parameterTypes[p]; if (Utils.hasUnresolvableType(parameterType)) { throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", parameterType); } Annotation[] parameterAnnotations = parameterAnnotationsArray[p];//5 if (parameterAnnotations == null) { throw parameterError(p, "No Retrofit annotation found."); } parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations); } ... return new ServiceMethod<>(this); }
註釋1處調用了createCallAdapter方法,它最終會獲得咱們在構建Retrofit調用build方法時adapterFactories添加的對象的get方法,Retrofit的build方法部分代碼:
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories); adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
adapterFactories列表默認會添加defaultCallAdapterFactory,defaultCallAdapterFactory指的是ExecutorCallAdapterFactory,ExecutorCallAdapterFactory的get方法以下所示。
public CallAdapter<Call<?>> get(Type returnType, Annotation[] annotations, Retrofit retrofit) { if (getRawType(returnType) != Call.class) { return null; } final Type responseType = Utils.getCallResponseType(returnType); return new CallAdapter<Call<?>>() { @Override public Type responseType() { return responseType; } @Override public <R> Call<R> adapt(Call<R> call) { return new ExecutorCallbackCall<>(callbackExecutor, call); } }; }
get方法會獲得CallAdapter對象,它的responseType方法會返回數據的真實類型,好比 Call<IpModel>
,它就會返回IpModel。adapt方法會建立ExecutorCallbackCall,它會將call的回調轉發至UI線程。
接着回到ServiceMethod的 build方法,註釋2處調用CallAdapter的responseType獲得的是返回數據的真實類型。
註釋3處調用createResponseConverter方法來遍歷converterFactories列表中存儲的Converter.Factory,並返回一個合適的Converter用來轉換對象。此前咱們在構建Retrofit 調用了addConverterFactory(GsonConverterFactory.create())將GsonConverterFactory(Converter.Factory的子類)添加到converterFactories列表中,表示返回的數據支持轉換爲Json對象。
註釋4處遍歷parseMethodAnnotation方法來對請求方式(好比GET、POST)和請求地址進行解析。註釋5處對方法中的參數註解進行解析(好比@Query、@Part)。最後建立ServiceMethod類並返回。
接下來回過頭來查看Retrofit的create方法,在調用了loadServiceMethod方法後會建立OkHttpCall,OkHttpCall的構造函數只是進行了賦值操做。緊接着調用serviceMethod.callAdapter.adapt(okHttpCall)
,callAdapter的adapt方法前面講過,它會建立ExecutorCallbackCall,ExecutorCallbackCall的部分代碼以下所示。
ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
this.callbackExecutor = callbackExecutor; this.delegate = delegate; } @Override public void enqueue(final Callback<T> callback) { if (callback == null) throw new NullPointerException("callback == null"); delegate.enqueue(new Callback<T>() {//1 @Override public void onResponse(Call<T> call, final Response<T> response) { callbackExecutor.execute(new Runnable() { @Override public void run() { if (delegate.isCanceled()) { callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled")); } else { callback.onResponse(ExecutorCallbackCall.this, response); } } }); } @Override public void onFailure(Call<T> call, final Throwable t) { callbackExecutor.execute(new Runnable() { @Override public void run() { callback.onFailure(ExecutorCallbackCall.this, t); } }); } }); }
能夠看出ExecutorCallbackCall是對Call的封裝,它主要添加了經過callbackExecutor將請求回調到UI線程。
當咱們獲得Call對象後會調用它的enqueue方法,其實調用的是ExecutorCallbackCall的enqueue方法,而從註釋1處能夠看出ExecutorCallbackCall的enqueue方法最終調用的是delegate的enqueue方法。delegate從Retrofit的create方法的代碼中咱們知道它其實就是OkHttpCall。
接下來咱們就來查看OkHttpCall的enqueue方法,代碼以下所示。
public void enqueue(final Callback<T> callback) { if (callback == null) throw new NullPointerException("callback == null"); okhttp3.Call call; ... call.enqueue(new okhttp3.Callback() {//1 @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) throws IOException { Response<T> response; try { response = parseResponse(rawResponse);//2 } catch (Throwable e) { callFailure(e); return; } callSuccess(response); } ... }
註釋1處調用了okhttp3.Call的enqueue方法。註釋2處調用parseResponse方法:
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException { ResponseBody rawBody = rawResponse.body(); ... int code = rawResponse.code(); if (code < 200 || code >= 300) { try { ResponseBody bufferedBody = Utils.buffer(rawBody); return Response.error(bufferedBody, rawResponse); } finally { rawBody.close(); } } if (code == 204 || code == 205) { return Response.success(null, rawResponse); } ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody); try { T body = serviceMethod.toResponse(catchingBody);//2 return Response.success(body, rawResponse); } catch (RuntimeException e) { catchingBody.throwIfCaught(); throw e; } }
根據返回的不一樣的狀態碼code值來作不一樣的操做,若是順利則會調用註釋2處的代碼,接下來看toResponse方法裏作了什麼:
T toResponse(ResponseBody body) throws IOException { return responseConverter.convert(body); }
這個responseConverter就是此前講過在ServiceMethod的build方法調用createResponseConverter方法返回的Converter,在此前的例子中咱們傳入的是GsonConverterFactory,所以能夠查看GsonConverterFactory的代碼,以下所示。
public final class GsonConverterFactory extends Converter.Factory { ... @Override public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); return new GsonResponseBodyConverter<>(gson, adapter); } ... }
在GsonConverterFactory 中有一個方法responseBodyConverter,它最終會建立GsonResponseBodyConverter:
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> { private final Gson gson; private final TypeAdapter<T> adapter; GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) { this.gson = gson; this.adapter = adapter; } @Override public T convert(ResponseBody value) throws IOException { JsonReader jsonReader = gson.newJsonReader(value.charStream()); try { return adapter.read(jsonReader); } finally { value.close(); } } }
在GsonResponseBodyConverter的convert方法裏會將回調的數據轉換爲Json格式。所以咱們也知道了此前調用responseConverter.convert
是爲了轉換爲特定的數據格式。
Call的enqueue方法主要作的就是用OKHttp來請求網絡並將返回的Response進行數據轉換並回調給UI線程。
至此,Retrofit的源碼就講到這裏。
網絡請求開源庫是一個將 網絡請求的相關功能封裝好的類庫
沒有網絡請求框架以前
App想與服務器進行網絡請求交互是一件很痛苦的事:由於Android的主線程不能進行網絡請求,需另開1個線程請求、考慮到線程池,緩存等一堆問題
使用網絡請求庫後
實現網絡請求的需求同時不須要考慮:
網絡請求庫的本質 = 封裝了 網絡請求 + 異步 + 數據處理功能的庫
其中,網絡請求功能則是採用Android
網絡請求的原生方法(HttpClient
或HttpURLConnection
)
具體以下圖
現在Android
中主流的網絡請求框架有:
Android-Async-Http
Volley
OkHttp
Retrofit
下面是簡單介紹:
Github地址
引用:
★★★Android網絡編程(一)HTTP協議原理
★★★Android網絡編程(二)HttpClient與HttpURLConnection
★★★Android網絡編程(三)Volley用法全解析
★★★Android網絡編程(四)從源碼解析volley
★★★Android網絡編程(五)OkHttp2.x用法全解析
★★★Android網絡編程(六)OkHttp3用法全解析
★★★Android網絡編程(七)源碼解析OkHttp前篇-請求網絡
★★★Android網絡編程(八)源碼解析OkHttp後篇-複用鏈接池
★★★Android網絡編程(九)Retrofit2前篇-基本使用
★★★Android網絡編程(十)Retrofit2後篇-註解
★★★Android網絡編程(十一)源碼解析Retrofit
★★★Android:主流網絡請求開源庫的對比(Android-Async-Http、Volley、OkHttp、Retrofit)