Android網絡請求心路歷程

HTTP請求&響應

既然說從入門級開始就說說Http請求包的結構。
一次請求就是向目標服務器發送一串文本。什麼樣的文本?有下面結構的文本。
HTTP請求包結構php

blob.png

例子:html

1
2
3
4
5
6
7
     POST /meme.php/home/user/login HTTP/1.1
     Host: 114.215.86.90
     Cache-Control: no-cache
     Postman-Token: bd243d6b-da03-902f-0a2c-8e9377f6f6ed
     Content-Type: application/x-www-form-urlencoded
 
     tel=13637829200&password=123456

請求了就會收到響應包(若是對面存在HTTP服務器)
HTTP響應包結構android

blob.png

例子:git

1
2
3
4
5
6
7
8
9
10
     HTTP/1.1 200 OK
     Date: Sat, 02 Jan 2016 13:20:55 GMT
     Server: Apache/2.4.6 (CentOS) PHP/5.6.14
     X-Powered-By: PHP/5.6.14
     Content-Length: 78
     Keep-Alive: timeout=5, max=100
     Connection: Keep-Alive
     Content-Type: application/json; charset=utf-8
 
     { "status" :202, "info" : "\u6b64\u7528\u6237\u4e0d\u5b58\u5728\uff01" , "data" : null }

Http請求方式有github

方法 描述
GET 請求指定url的數據,請求體爲空(例如打開網頁)。
POST 請求指定url的數據,同時傳遞參數(在請求體中)。
HEAD 相似於get請求,只不過返回的響應體爲空,用於獲取響應頭。
PUT 從客戶端向服務器傳送的數據取代指定的文檔的內容。
DELETE 請求服務器刪除指定的頁面。
CONNECT HTTP/1.1協議中預留給可以將鏈接改成管道方式的代理服務器。
OPTIONS 容許客戶端查看服務器的性能。
TRACE 回顯服務器收到的請求,主要用於測試或診斷。

經常使用只有Post與Get。web

Get&Post

網絡請求中咱們經常使用鍵值對來傳輸參數(少部分api用json來傳遞,畢竟不是主流)。
經過上面的介紹,能夠看出雖然Post與Get本意一個是表單提交一個是請求頁面,但本質並無什麼區別。下面說說參數在這2者的位置。面試

  • Get方式
    在url中填寫參數:算法

    1
       http: //xxxx.xx.com/xx.php?params1=value1&params2=value2

    甚至使用路由編程

    1
       http: //xxxx.xx.com/xxx/value1/value2/value3

    這些就是web服務器框架的事了。json

  • Post方式
    參數是通過編碼放在請求體中的。編碼包括x-www-form-urlencoded 與 form-data。
    x-www-form-urlencoded的編碼方式是這樣:

    1
       tel=13637829200&password=123456

    form-data的編碼方式是這樣:

    1
    2
    3
    4
    5
    6
    7
    8
    9
       ----WebKitFormBoundary7MA4YWxkTrZu0gW
       Content-Disposition: form-data; name= "tel"
     
       13637829200
       ----WebKitFormBoundary7MA4YWxkTrZu0gW
       Content-Disposition: form-data; name= "password"
     
       123456
       ----WebKitFormBoundary7MA4YWxkTrZu0gW

    x-www-form-urlencoded的優越性就很明顯了。不過x-www-form-urlencoded只能傳鍵值對,可是form-data能夠傳二進制

由於url是存在於請求行中的。
因此Get與Post區別本質就是參數是放在請求行中仍是放在請求體中
固然不管用哪一種都能放在請求頭中。通常在請求頭中放一些發送端的常量。

有人說:

  • Get是明文,Post隱藏
    移動端不是瀏覽器,不用https全都是明文。

  • Get傳遞數據上限XXX
    胡說。有限制的是瀏覽器中的url長度,不是Http協議,移動端請求無影響。

  • Get中文須要編碼
    是真的...要注意。URLEncoder.encode(params, "gbk");

仍是建議用post規範參數傳遞方式。並無什麼更優秀,只是你們都這樣社會更和諧。

上面說的是請求。下面說響應。
請求是鍵值對,但返回數據咱們經常使用Json。
對於內存中的結構數據,確定要用數據描述語言將對象序列化成文本,再用Http傳遞,接收端並從文本還原成結構數據。
對象(服務器)<-->文本(Http傳輸)<-->對象(移動端) 。

服務器返回的數據大部分都是複雜的結構數據,因此Json最適合。
Json解析庫有不少Google的Gson,阿里的FastJson
Gson的用法看這裏

 

HttpClient & HttpURLConnection

HttpClient早被廢棄了,誰更好這種問題也只有經驗落後的面試官纔會問。具體緣由能夠看這裏

下面說說HttpURLConnection的用法。
最開始接觸的就是這個。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
   public class NetUtils {
         public static String post(String url, String content) {
             HttpURLConnection conn =  null ;
             try  {
                 // 建立一個URL對象
                 URL mURL =  new  URL(url);
                 // 調用URL的openConnection()方法,獲取HttpURLConnection對象
                 conn = (HttpURLConnection) mURL.openConnection();
 
                 conn.setRequestMethod( "POST" ); // 設置請求方法爲post
                 conn.setReadTimeout(5000); // 設置讀取超時爲5秒
                 conn.setConnectTimeout(10000); // 設置鏈接網絡超時爲10秒
                 conn.setDoOutput( true ); // 設置此方法,容許向服務器輸出內容
 
                 // post請求的參數
                 String data = content;
                 // 得到一個輸出流,向服務器寫數據,默認狀況下,系統不容許向服務器輸出內容
                 OutputStream out = conn.getOutputStream(); // 得到一個輸出流,向服務器寫數據
                 out.write(data.getBytes());
                 out.flush();
                 out.close();
 
                 int responseCode = conn.getResponseCode(); // 調用此方法就沒必要再使用conn.connect()方法
                 if  (responseCode == 200) {
 
                     InputStream is = conn.getInputStream();
                     String response = getStringFromInputStream(is);
                     return  response;
                 else  {
                     throw  new  NetworkErrorException( "response status is " +responseCode);
                 }
 
             catch  (Exception e) {
                 e.printStackTrace();
             } finally {
                 if  (conn !=  null ) {
                     conn.disconnect(); // 關閉鏈接
                 }
             }
 
             return  null ;
         }
 
         public static String get(String url) {
             HttpURLConnection conn =  null ;
             try  {
                 // 利用string url構建URL對象
                 URL mURL =  new  URL(url);
                 conn = (HttpURLConnection) mURL.openConnection();
 
                 conn.setRequestMethod( "GET" );
                 conn.setReadTimeout(5000);
                 conn.setConnectTimeout(10000);
 
                 int responseCode = conn.getResponseCode();
                 if  (responseCode == 200) {
 
                     InputStream is = conn.getInputStream();
                     String response = getStringFromInputStream(is);
                     return  response;
                 else  {
                     throw  new  NetworkErrorException( "response status is " +responseCode);
                 }
 
             catch  (Exception e) {
                 e.printStackTrace();
             } finally {
 
                 if  (conn !=  null ) {
                     conn.disconnect();
                 }
             }
 
             return  null ;
         }
 
         private static String getStringFromInputStream(InputStream is)
                 throws IOException {
             ByteArrayOutputStream os =  new  ByteArrayOutputStream();
             // 模板代碼 必須熟練
             byte[] buffer =  new  byte[1024];
             int len = -1;
             while  ((len = is.read(buffer)) != -1) {
                 os.write(buffer, 0, len);
             }
             is.close();
             String state = os.toString(); // 把流中的數據轉換成字符串,採用的編碼是utf-8(模擬器默認編碼)
             os.close();
             return  state;
         }
     }

注意網絡權限!被坑了多少次。

1
<uses-permission android:name= "android.permission.INTERNET" />

同步&異步

這2個概念僅存在於多線程編程中。
android中默認只有一個主線程,也叫UI線程。由於View繪製只能在這個線程內進行。
因此若是你阻塞了(某些操做使這個線程在此處運行了N秒)這個線程,這期間View繪製將不能進行,UI就會卡。因此要極力避免在UI線程進行耗時操做。
網絡請求是一個典型耗時操做。
經過上面的Utils類進行網絡請求只有一行代碼。

1
NetUtils.get( "http://www.baidu.com" );//這行代碼將執行幾百毫秒。

若是你這樣寫

1
2
3
4
5
6
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         String response = Utils.get( "http://www.baidu.com" );
     }

就會死。。
這就是同步方式。直接耗時操做阻塞線程直到數據接收完畢而後返回。Android不容許的。
異步方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
          //在主線程new的Handler,就會在主線程進行後續處理。
     private Handler handler =  new  Handler();
     private TextView textView;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         textView = (TextView) findViewById(R.id.text);
         new  Thread( new  Runnable() {
             @Override
             public void run() {
                     //從網絡獲取數據
                 final String response = NetUtils.get( "http://www.baidu.com" );
                     //向Handler發送處理操做
                 handler.post( new  Runnable() {
                     @Override
                     public void run() {
                             //在UI線程更新UI
                         textView.setText(response);
                     }
                 });
             }
         }).start();
     }

在子線程進行耗時操做,完成後經過Handler將更新UI的操做發送到主線程執行。這就叫異步。Handler是一個Android線程模型中重要的東西,與網絡無關便不說了。關於Handler不瞭解就先去Google一下。
關於Handler原理一篇不錯的文章

但這樣寫好難看。異步一般伴隨者他的好基友回調。
這是經過回調封裝的Utils類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AsynNetUtils {
         public interface Callback{
             void onResponse(String response);
         }
 
         public static void get(final String url, final Callback callback){
             final Handler handler =  new  Handler();
             new  Thread( new  Runnable() {
                 @Override
                 public void run() {
                     final String response = NetUtils.get(url);
                     handler.post( new  Runnable() {
                         @Override
                         public void run() {
                             callback.onResponse(response);
                         }
                     });
                 }
             });
         }
 
         public static void post(final String url, final String content, final Callback callback){
             final Handler handler =  new  Handler();
             new  Thread( new  Runnable() {
                 @Override
                 public void run() {
                     final String response = NetUtils.post(url,content);
                     handler.post( new  Runnable() {
                         @Override
                         public void run() {
                             callback.onResponse(response);
                         }
                     });
                 }
             });
         }
     }

而後使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
     private TextView textView;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         textView = (TextView) findViewById(R.id.webview);
         AsynNetUtils.get( "http://www.baidu.com" new  AsynNetUtils.Callback() {
             @Override
             public void onResponse(String response) {
                 textView.setText(response);
             }
         });

是否是優雅不少。
嗯,一個蠢到哭的網絡請求方案成型了。
愚蠢的地方有不少:

  • 每次都new Thread,new Handler消耗過大

  • 沒有異常處理機制

  • 沒有緩存機制

  • 沒有完善的API(請求頭,參數,編碼,攔截器等)與調試模式

  • 沒有Https

HTTP緩存機制

緩存對於移動端是很是重要的存在。

  • 減小請求次數,減少服務器壓力.

  • 本地數據讀取速度更快,讓頁面不會空白幾百毫秒。

  • 在無網絡的狀況下提供數據。

緩存通常由服務器控制(經過某些方式能夠本地控制緩存,好比向過濾器添加緩存控制信息)。經過在請求頭添加下面幾個字端:

Request

請求頭字段 意義
If-Modified-Since: Sun, 03 Jan 2016 03:47:16 GMT 緩存文件的最後修改時間。
If-None-Match: "3415g77s19tc3:0" 緩存文件的Etag(Hash)值
Cache-Control: no-cache 不使用緩存
Pragma: no-cache 不使用緩存

Response

響應頭字段 意義
Cache-Control: public 響應被共有緩存,移動端無用
Cache-Control: private 響應被私有緩存,移動端無用
Cache-Control:no-cache 不緩存
Cache-Control:no-store 不緩存
Cache-Control: max-age=60 60秒以後緩存過時(相對時間)
Date: Sun, 03 Jan 2016 04:07:01 GMT 當前response發送的時間
Expires: Sun, 03 Jan 2016 07:07:01 GMT 緩存過時的時間(絕對時間)
Last-Modified: Sun, 03 Jan 2016 04:07:01 GMT 服務器端文件的最後修改時間
ETag: "3415g77s19tc3:0" 服務器端文件的Etag[Hash]值

正式使用時按需求也許只包含其中部分字段。
客戶端要根據這些信息儲存此次請求信息。
而後在客戶端發起請求的時候要檢查緩存。遵循下面步驟:

blob.png

注意服務器返回304意思是數據沒有變更滾去讀緩存信息。
曾經年輕的我爲本身寫的網絡請求框架添加完善了緩存機制,還沾沾自喜,直到有一天我看到了下面2個東西。(/TДT)/

Volley&OkHttp

Volley&OkHttp應該是如今最經常使用的網絡請求庫。用法也很是類似。都是用構造請求加入請求隊列的方式管理網絡請求。

先說Volley:
Volley能夠經過這個庫進行依賴.
Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及如下版本,使用的是HttpClient。
Volley的基本用法,網上資料無數,這裏推薦郭霖大神的博客
Volley存在一個緩存線程,一個網絡請求線程池(默認4個線程)。
Volley這樣直接用開發效率會比較低,我將我使用Volley時的各類技巧封裝成了一個庫RequestVolly.
我在這個庫中將構造請求的方式封裝爲了函數式調用。維持一個全局的請求隊列,拓展一些方便的API。

不過再怎麼封裝Volley在功能拓展性上始終沒法與OkHttp相比。
Volley中止了更新,而OkHttp獲得了官方的承認,並在不斷優化。
所以我最終替換爲了OkHttp

OkHttp用法見這裏
很友好的API與詳盡的文檔。
這篇文章也寫的很詳細了。
OkHttp使用Okio進行數據傳輸。都是Square家的。
但並非直接用OkHttp。Square公司還出了一個Retrofit庫配合OkHttp戰鬥力翻倍。

Retrofit&RestAPI

Retrofit極大的簡化了網絡請求的操做,它應該說只是一個Rest API管理庫,它是直接使用OKHttp進行網絡請求並不影響你對OkHttp進行配置。畢竟都是Square公司出品。
RestAPI是一種軟件設計風格。
服務器做爲資源存放地。客戶端去請求GET,PUT, POST,DELETE資源。而且是無狀態的,沒有session的參與。
移動端與服務器交互最重要的就是API的設計。好比這是一個標準的登陸接口。

blob.png

大家應該看的出這個接口對應的請求包與響應包大概是什麼樣子吧。
請求方式,請求參數,響應數據,都很清晰。
使用Retrofit這些API能夠直觀的體如今代碼中。

blob.png

而後使用Retrofit提供給你的這個接口的實現類 就能直接進行網絡請求得到結構數據。

注意Retrofit2.0相較1.9進行了大量不兼容更新。google上大部分教程都是基於1.9的。這裏有個2.0的教程。

教程裏進行異步請求是使用Call。Retrofit最強大的地方在於支持RxJava。就像我上圖中返回的是一個Observable。RxJava上手難度比較高,但用過就再也離不開了。Retrofit+OkHttp+RxJava配合框架打出成噸的輸出,這裏再也不多說。

網絡請求學習到這裏我以爲已經到頂了。。

網絡圖片加載優化

對於圖片的傳輸,就像上面的登陸接口的avatar字段,並不會直接把圖片寫在返回內容裏,而是給一個圖片的地址。須要時再去加載。

若是你直接用HttpURLConnection去取一張圖片,你辦獲得,不過沒優化就只是個BUG不斷demo。絕對不能正式使用。
注意網絡圖片有些特色:

  1. 它永遠不會變
    一個連接對應的圖片通常永遠不會變,因此當第一次加載了圖片時,就應該予以永久緩存,之後就再也不網絡請求。

  2. 它很佔內存
    一張圖片小的幾十k多的幾M高清無碼。尺寸也是64*64到2k圖。你不能就這樣直接顯示到UI,甚至不能直接放進內存。

  3. 它要加載好久
    加載一張圖片須要幾百ms到幾m。這期間的UI佔位圖功能也是必須考慮的。

說說我在上面提到的RequestVolley裏作的圖片請求處理(沒錯我作了,這部分的代碼能夠去github裏看源碼)。

三級緩存

網上常說三級緩存--服務器,文件,內存。不過我以爲服務器不算是一級緩存,那就是數據源嘛。

  • 內存緩存
    首先內存緩存使用LruCache。LRU是Least Recently Used 近期最少使用算法,這裏肯定一個大小,當Map裏對象大小總和大於這個大小時將使用頻率最低的對象釋放。我將內存大小限制爲進程可用內存的1/8.
    內存緩存裏讀獲得的數據就直接返回,讀不到的向硬盤緩存要數據。

  • 硬盤緩存
    硬盤緩存使用DiskLruCache。這個類不在API中。得複製使用。
    看見LRU就明白了吧。我將硬盤緩存大小設置爲100M。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
       @Override
       public void putBitmap(String url, Bitmap bitmap) {
           put(url, bitmap);       //向內存Lru緩存存放數據時,主動放進硬盤緩存裏
           try  {
               Editor editor = mDiskLruCache.edit(hashKeyForDisk(url));
               bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));
               editor.commit();
           catch  (IOException e) {
               e.printStackTrace();
           }
       }   //當內存Lru緩存中沒有所需數據時,調用創造。
       @Override
       protected Bitmap create(String url) {       //獲取key
           String key = hashKeyForDisk(url);       //從硬盤讀取數據
           Bitmap bitmap =  null ;       try  {
               DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);           if (snapShot!= null ){
                   bitmap = BitmapFactory.decodeStream(snapShot.getInputStream(0));
               }
           catch  (IOException e) {
               e.printStackTrace();
           }       return  bitmap;
       }

    DiskLruCache的原理再也不解釋了(我還解決了它存在的一個BUG,向Log中添加的數據增刪記錄時,最後一條沒有輸出,致使最後一條緩存一直失效。)

  • 硬盤緩存也沒有數據就返回空,而後就向服務器請求數據。

這就是整個流程。
但我這樣的處理方案仍是有不少侷限。

  • 圖片未經壓縮處理直接存儲使用

  • 文件操做在主線程

  • 沒有完善的圖片處理API

之前也以爲這樣已經足夠好直到我遇到下面倆。

Fresco&Glide

不用想也知道它們都作了很是完善的優化,重複造輪子的行爲很蠢。
Fresco是Facebook公司的黑科技。光看功能介紹就看出很是強大。使用方法官方博客說的夠詳細了。
真三級緩存,變換後的BItmap(內存),變換前的原始圖片(內存),硬盤緩存。
在內存管理上作到了極致。對於重度圖片使用的APP應該是很是好的。
它通常是直接使用SimpleDraweeView來替換ImageView,呃~侵入性較強,依賴上它apk包直接大1M。代碼量驚人。

因此我更喜歡Glide,做者是bumptech。這個庫被普遍的運用在google的開源項目中,包括2014年google I/O大會上發佈的官方app。
這裏有詳細介紹。直接使用ImageView便可,無需初始化,極簡的API,豐富的拓展,鏈式調用都是我喜歡的。
豐富的拓展指的就是這個
另外我也用過Picasso。API與Glide簡直如出一轍,功能略少,且有半年未修復的BUG。

圖片管理方案

再說說圖片存儲。不要存在本身服務器上面,徒增流量壓力,尚未圖片處理功能。
推薦七牛阿里雲存儲(沒用過其它 π__π )。它們都有很重要的一項圖片處理。在圖片Url上加上參數來對圖片進行一些處理再傳輸。
因而(七牛的處理代碼)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
     public static String getSmallImage(String image){
         if  (image== null ) return  null ;
         if  (isQiniuAddress(image)) image+= "?imageView2/0/w/" +IMAGE_SIZE_SMALL;
         return  image;
     }
 
     public static String getLargeImage(String image){
         if  (image== null ) return  null ;
         if  (isQiniuAddress(image)) image+= "?imageView2/0/w/" +IMAGE_SIZE_LARGE;
         return  image;
     }
 
     public static String getSizeImage(String image,int width){
         if  (image== null ) return  null ;
         if  (isQiniuAddress(image)) image+= "?imageView2/0/w/" +width;
         return  image;
     }

既能夠加快請求速度,又能減小流量。再配合Fresco或Glide。完美的圖片加載方案。不過這就須要你把全部圖片都存放在七牛或阿里雲,這樣也不錯。

相關文章
相關標籤/搜索