Android技術架構之網路請求心路歷程(可收藏)
前言 Android架構
Android架構技術介紹
- 架構與設計
- 設計模式
- 重構
- 網路程式設計框架
- TCP格式三次握手與四次揮手
- HttpClient
- HttpURLConnection
- Volley
- OkHttp
- Retrofit
- UI架構模式
- MVC
- MVP
- MVVM
文末有相關技術福利,需要的可以領取。
HTTP請求&響應
既然說從入門級開始就說說Http請求包的結構。
一次請求就是向目標伺服器傳送一串文字。什麼樣的文字?有下面結構的文字。
HTTP請求包結構
請求包
請求了就會收到響應包(如果對面存在HTTP伺服器)
HTTP響應包結構
響應包
Http請求方式有
常用只有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); } }); } }).start(); } 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); } }); } }).start(); } }
然後使用方法。
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
Response
正式使用時按需求也許只包含其中部分欄位。
客戶端要根據這些資訊儲存這次請求資訊。
然後在客戶端發起請求的時候要檢查快取。遵循下面步驟:
瀏覽器快取機制
注意伺服器返回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的設計。比如這是一個標準的登入介面。
Paste_Image.png
你們應該看的出這個介面對應的請求包與響應包大概是什麼樣子吧。
請求方式,請求引數,響應資料,都很清晰。
使用Retrofit這些API可以直觀的體現在程式碼中。
Paste_Image.png
然後使用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是Facebook公司的黑科技。光看功能介紹就看出非常強大。使用方法官方部落格說的夠詳細了。
真三級快取,變換後的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; }
最後
我這邊整理了一份Android架構學習思維路線圖,供各位學習者參考,如有需要可以收藏。
轉發+關注,然後私信回覆我“資料”獲取以上高清技術思維圖,以及相關技術的免費視訊學習資料。