1. 程式人生 > >Android技術架構之網路請求心路歷程(可收藏)

Android技術架構之網路請求心路歷程(可收藏)

前言 Android架構

Android架構技術介紹

  • 架構與設計
  • 設計模式
  • 重構
  1. 網路程式設計框架
  2. TCP格式三次握手與四次揮手
  3. HttpClient
  4. HttpURLConnection
  5. Volley
  6. OkHttp
  7. Retrofit
  • UI架構模式
  1. MVC
  2. MVP
  3. MVVM

文末有相關技術福利,需要的可以領取。

HTTP請求&響應

既然說從入門級開始就說說Http請求包的結構。

一次請求就是向目標伺服器傳送一串文字。什麼樣的文字?有下面結構的文字。

HTTP請求包結構

Android技術架構之網路請求心路歷程(可收藏)

請求包

請求了就會收到響應包(如果對面存在HTTP伺服器)

HTTP響應包結構

Android技術架構之網路請求心路歷程(可收藏)

響應包

Http請求方式有

Android技術架構之網路請求心路歷程(可收藏)

 

常用只有Post與Get。

Get&Post

網路請求中我們常用鍵值對來傳輸引數(少部分api用json來傳遞,畢竟不是主流)。

通過上面的介紹,可以看出雖然Post與Get本意一個是表單提交一個是請求頁面,但本質並沒有什麼區別。下面說說引數在這2者的位置。

  • Get方式
  • 在url中填寫引數:
 http://xxxx.xx.com/xx.php?params1=value1&params2=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

Android技術架構之網路請求心路歷程(可收藏)

 

Response

Android技術架構之網路請求心路歷程(可收藏)

 

正式使用時按需求也許只包含其中部分欄位。

客戶端要根據這些資訊儲存這次請求資訊。

然後在客戶端發起請求的時候要檢查快取。遵循下面步驟:

Android技術架構之網路請求心路歷程(可收藏)

瀏覽器快取機制

注意伺服器返回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的設計。比如這是一個標準的登入介面。

Android技術架構之網路請求心路歷程(可收藏)

Paste_Image.png

你們應該看的出這個介面對應的請求包與響應包大概是什麼樣子吧。

請求方式,請求引數,響應資料,都很清晰。

使用Retrofit這些API可以直觀的體現在程式碼中。

Android技術架構之網路請求心路歷程(可收藏)

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。絕對不能正式使用。

注意網路圖片有些特點:

  1. 它永遠不會變
  2. 一個連結對應的圖片一般永遠不會變,所以當第一次載入了圖片時,就應該予以永久快取,以後就不再網路請求。
  • 它很佔記憶體
  • 一張圖片小的幾十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架構學習思維路線圖,供各位學習者參考,如有需要可以收藏。

Android技術架構之網路請求心路歷程(可收藏)

 

轉發+關注,然後私信回覆我“資料”獲取以上高清技術思維圖,以及相關技術的免費視訊學習資料。