Android網絡請求心路歷程
網絡請求是android客戶端很重要的部分。下面從入門級開始介紹下自己Android網絡請求的實踐歷程。希望能給剛接觸Android網絡部分的朋友一些幫助。
本文包含:
- HTTP請求&響應
- Get&Post
- HttpClient & HttpURLConnection
- 同步&異步
- HTTP緩存機制
- Volley&OkHttp
- Retrofit&RestAPI
- 網絡圖片加載優化
- Fresco&Glide
- 圖片管理方案
HTTP請求&響應
既然說從入門級開始就說說Http請求包的結構。
一次請求就是向目標服務器發送一串文本。什么樣的文本?有下面結構的文本。
HTTP請求包結構

例子:
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響應包結構

例子:
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請求方式有
方法 | 描述 |
---|---|
GET | 請求指定url的數據,請求體為空(例如打開網頁)。 |
POST | 請求指定url的數據,同時傳遞參數(在請求體中)。 |
HEAD | 類似于get請求,只不過返回的響應體為空,用于獲取響應頭。 |
PUT | 從客戶端向服務器傳送的數據取代指定的文檔的內容。 |
DELETE | 請求服務器刪除指定的頁面。 |
CONNECT | HTTP/1.1協議中預留給能夠將連接改為管道方式的代理服務器。 |
OPTIONS | 允許客戶端查看服務器的性能。 |
TRACE | 回顯服務器收到的請求,主要用于測試或診斷。 |
常用只有Post與Get。
Get&Post
網絡請求中我們常用鍵值對來傳輸參數(少部分api用json來傳遞,畢竟不是主流)。
通過上面的介紹,可以看出雖然Post與Get本意一個是表單提交一個是請求頁面,但本質并沒有什么區別。下面說說參數在這2者的位置。
-
Get方式
在url中填寫參數:http://xxxx.xx.com/xx.php?params1=value1¶ms2=value2
甚至使用路由
http://xxxx.xx.com/xxx/value1/value2/value3
這些就是web服務器框架的事了。
-
Post方式
參數是經過編碼放在請求體中的。編碼包括x-www-form-urlencoded
與form-data
。
x-www-form-urlencoded
的編碼方式是這樣:tel=13637829200&password=123456
form-data
的編碼方式是這樣:----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協議,移動端請求無影響。Http服務器部分有限制的設置一下即可。 - Get中文需要編碼
是真的...要注意。URLEncoder.encode(params, "gbk");
還是建議用post規范參數傳遞方式。并沒有什么更優秀,只是大家都這樣社會更和諧。
上面說的是請求。下面說響應。
請求是鍵值對,但返回數據我們常用Json。
對于內存中的結構數據,肯定要用數據描述語言將對象序列化成文本,再用Http傳遞,接收端并從文本還原成結構數據。
對象(服務器)<-->文本(Http傳輸)<-->對象(移動端) 。
服務器返回的數據大部分都是復雜的結構數據,所以Json最適合。
Json解析庫有很多Google的Gson,阿里的FastJson。
Gson的用法看這里。
HttpClient & HttpURLConnection
HttpClient早被廢棄了,誰更好這種問題也只有經驗落后的面試官才會問。具體原因可以看這里。
下面說說HttpURLConnection的用法。
最開始接觸的就是這個。
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; } }
注意網絡權限!被坑了多少次。
<uses-permission android:name="android.permission.INTERNET"/>
同步&異步
這2個概念僅存在于多線程編程中。
android中默認只有一個主線程,也叫UI線程。因為View繪制只能在這個線程內進行。
所以如果你阻塞了(某些操作使這個線程在此處運行了N秒)這個線程,這期間View繪制將不能進行,UI就會卡。所以要極力避免在UI線程進行耗時操作。
網絡請求是一個典型耗時操作。
通過上面的Utils類進行網絡請求只有一行代碼。
NetUtils.get("http://www.baidu.com");//這行代碼將執行幾百毫秒。
如果你這樣寫
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String response = Utils.get("http://www.baidu.com"); }
就會死。。
這就是同步方式。直接耗時操作阻塞線程直到數據接收完畢然后返回。Android不允許的。
異步方式:
//在主線程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類。
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); } }); } }); } }
然后使用方法。
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]值 |
正式使用時按需求也許只包含其中部分字段。
客戶端要根據這些信息儲存這次請求信息。
然后在客戶端發起請求的時候要檢查緩存。遵循下面步驟:

注意服務器返回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的設計。比如這是一個標準的登錄接口。

你們應該看的出這個接口對應的請求包與響應包大概是什么樣子吧。
請求方式,請求參數,響應數據,都很清晰。
使用Retrofit這些API可以直觀的體現在代碼中。

然后使用Retrofit提供給你的這個接口的實現類 就能直接進行網絡請求獲得結構數據。
注意Retrofit2.0相較1.9進行了大量不兼容更新。google上大部分教程都是基于1.9的。這里有個2.0的教程。
教程里進行異步請求是使用Call。Retrofit最強大的地方在于支持RxJava。就像我上圖中返回的是一個Observable。RxJava上手難度比較高,但用過就再也離不開了。Retrofit+OkHttp+RxJava配合框架打出成噸的輸出,這里不再多說。
網絡請求學習到這里我覺得已經到頂了。。
網絡圖片加載優化
對于圖片的傳輸,就像上面的登錄接口的avatar字段,并不會直接把圖片寫在返回內容里,而是給一個圖片的地址。需要時再去加載。
如果你直接用HttpURLConnection去取一張圖片,你辦得到,不過沒優化就只是個BUG不斷demo。絕對不能正式使用。
注意網絡圖片有些特點:
- 它永遠不會變
一個鏈接對應的圖片一般永遠不會變,所以當第一次加載了圖片時,就應該予以永久緩存,以后就不再網絡請求。 - 它很占內存
一張圖片小的幾十k多的幾M高清無碼。尺寸也是64*64到2k圖。你不能就這樣直接顯示到UI,甚至不能直接放進內存。 - 它要加載很久
加載一張圖片需要幾百ms到幾m。這期間的UI占位圖功能也是必須考慮的。
說說我在上面提到的RequestVolley里做的圖片請求處理(沒錯我做了,這部分的代碼可以去github里看源碼)。
三級緩存
網上常說三級緩存--服務器,文件,內存。不過我覺得服務器不算是一級緩存,那就是數據源嘛。
-
內存緩存
首先內存緩存使用LruCache。LRU是Least Recently Used 近期最少使用算法,這里確定一個大小,當Map里對象大小總和大于這個大小時將使用頻率最低的對象釋放。我將內存大小限制為進程可用內存的1/8.
內存緩存里讀得到的數據就直接返回,讀不到的向硬盤緩存要數據。 -
硬盤緩存
硬盤緩存使用DiskLruCache。這個類不在API中。得復制使用。
看見LRU就明白了吧。我將硬盤緩存大小設置為100M。@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是非死book公司的黑科技。光看功能介紹就看出非常強大。使用方法官方博客說的夠詳細了。
真三級緩存,變換后的BItmap(內存),變換前的原始圖片(內存),硬盤緩存。
在內存管理上做到了極致。對于重度圖片使用的APP應該是非常好的。
它一般是直接使用SimpleDraweeView
來替換ImageView
,呃~侵入性較強,依賴上它apk包直接大1M。代碼量驚人。
所以我更喜歡Glide,作者是bumptech。這個庫被廣泛的運用在google的開源項目中,包括2014年google I/O大會上發布的官方app。
這里有詳細介紹。直接使用ImageView即可,無需初始化,極簡的API,豐富的拓展,鏈式調用都是我喜歡的。
豐富的拓展指的就是這個。
另外我也用過Picasso。API與Glide簡直一模一樣,功能略少,且有半年未修復的BUG。
圖片管理方案
再說說圖片存儲。不要存在自己服務器上面,徒增流量壓力,還沒有圖片處理功能。
推薦七牛與阿里云存儲(沒用過其它 π__π )。它們都有很重要的一項圖片處理。在圖片Url上加上參數來對圖片進行一些處理再傳輸。
于是(七牛的處理代碼)
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。完美的圖片加載方案。
不過這就需要你把所有圖片都存放在七牛或阿里云,這樣也不錯。
圖片/文件上傳也都是使用它們第三方存儲,它們都有SDK與官方文檔教你。
不過圖片一定要壓縮過后上傳。上傳1-2M大的高清照片沒意義。