網絡性能優化那些事

移動互聯網時代,用戶對網絡愈來愈依賴。雖然網絡環境在逐漸變好,但也對網絡的應用提出了更高的要求,同時開發人員對網絡的重視度卻在降低。確實 WiFi 場景下用戶的網絡質量變好了,並且用戶對網絡流量消耗的敏感度也在降低。android

因爲對網絡問題的忽視,在網絡狀況很差的狀況下,用戶體驗會極度降低,這時網絡性能優化變得尤其重要。做爲一名移動開發者,面對複雜多變的移動網絡咱們該如何去優化呢?程序員

優化哪些方面?

一個數據包從手機發出通過無線網絡、基站、互聯網最後到達咱們的服務器,其中任何一個環節出現問題都會影響用戶的體驗。用戶的網絡環境、基站的負載能力、DNS 服務器、CDN 節點的鏈接速度等這些因素,對移動端應用來講不受控制。移動端的網絡優化,主要分爲如下三個方面:web

  • 速度

在網絡正常或者良好的時候,怎樣更好地利用帶寬,進一步提高網絡請求的速度。面試

  • 弱網絡

移動端網絡複雜多變,在出現網絡鏈接不穩定的時候,怎樣最大程度保證網絡的連貫性。編程

  • 安全

網路安全不容忽視,怎樣有效防止被第三方劫持、竊聽甚至篡改。瀏覽器

除了這三個問題,咱們還可能會關心網絡請求形成的耗電、流量問題。對於速度、弱網絡以及安全的優化,又該從哪些方面入手呢?首先咱們應該搞清楚一個網絡請求的整個過程(對這個過程不熟悉的小夥伴,推薦看下《圖解 HTTP》)。網絡請求緩存

由上圖能夠看到,整個網絡請求主要分爲幾個步驟,而整個請求耗時能夠細分到每個步驟裏面。安全

  • DNS 解析

經過 DNS 服務器,拿到對應域名的 IP 地址。在這個步驟,咱們比較關注 DNS 解析耗時狀況、運營商 LocalDNS 的劫持、DNS 調度這些問題。性能優化

  • 建立鏈接

跟服務器創建鏈接,這裏包括 TCP 三次握手、TLS 密鑰協商等工做。多個 IP/端口該如何選擇、是否要使用 HTTPS、可否能夠減小甚至省下建立鏈接的時間,這些問題都是咱們優化的關鍵。服務器

  • 發送/接收數據

在成功創建鏈接以後,就能夠愉快地跟服務器交互,進行組裝數據、發送數據、接收數據、解析數據。咱們關注的是,如何根據網絡情況將帶寬利用好,怎樣快速地偵測到網絡延時,在弱網絡下如何調整包大小等問題。

  • 關閉鏈接

鏈接的關閉看起來很是簡單,其實這裏的水也很深。這裏主要關注主動關閉和被動關閉兩種狀況,通常咱們都但願客戶端能夠主動關閉鏈接。

所謂網絡優化,就是圍繞速度、弱網絡、安全這三個核心內容,減小每個步驟的耗時,打造快速、穩定且安全的高質量網絡。

網絡庫

在實際的開發工做中,咱們不多會像《UNIX 網絡編程》那樣直接去操做底層的網絡接口,通常都會使用網絡庫。Square 出品的 OkHttp 是目前最流行的 Android 網絡庫,它還被 Google 加入到 Android 系統內部,爲廣大開發者提供網絡服務。

網絡庫屏蔽的下層複雜的網絡接口,讓咱們能夠更高效的使用網絡請求,極大的提升了咱們的開發效率。我常常看到一些開發者會使用基於網絡庫再次封裝的開源庫,這裏很不建議開發者使用這些庫。

首先不清楚這些庫是否能徹底符合咱們的需求;而後這些庫的質量良莠不齊,每每在使用中遇到問題沒法快速修復。這裏強烈建議你們使用一手資源,推薦本身封裝,不只能夠提高開發效率,還能夠提升下本身的編碼水平。

大平臺網絡庫

據瞭解業內大廠蘑菇街、頭條、UC 瀏覽器都在 Chromium 網絡庫上作了二次開發,而微信 Mars 在弱網絡方面作了大量優化,拼多多、虎牙、鏈家、美麗說這些應用都在使用 Mars。

下面咱們來一塊兒對比下各個網絡庫的核心實現。網絡庫

爲何大廠都不使用 OkHttp 呢?主要是由於它不支持跨平臺,對於大型應用來講跨平臺是很是重要的。咱們不但願全部的優化 Android 和 iOS 都要各自去實現一套,不只浪費人力並且還容易出現問題。

對於大廠來講,不能只侷限在客戶端網絡庫的雙端統一上,網絡優化不只僅是客戶端的事情,因此通常都有統一的網絡中臺,它負責提供前臺一整套網絡解決方案。

阿里的 ACCS、螞蟻的 mPaas、攜程的網絡服務都是公司級的網絡中臺服務,這樣全部的網絡優化可讓整個集團的全部接入應用受益。

監聽網絡狀態

根據網絡狀態對網絡請求進行區別對待,2G 與 WiFi 狀態下網絡質量確定是不同的,那對應的網絡策略也應該是不同的。

例如:在 WiFi 場景下能夠進行數據的預取、一些統計的集中上傳等;而在 2G 場景下此類操做以及網絡請求的次數策略都應該調低。

網絡是否鏈接

經過 ConnectivityManager 能夠獲取當前是否已鏈接網絡。

public static boolean isNetworkConnected(Context context) {
  if (context == null) returnfalse;
  ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  if (manager == null) returnfalse;
  NetworkInfo networkInfo = manager.getActiveNetworkInfo();
  if (networkInfo == null) returnfalse;
  return networkInfo.isAvailable() && networkInfo.isConnected();
}

isAvailable() 與 isConnected() 的區別:

狀態 isConnected() isAvailable()
顯示鏈接已保存,但標題欄沒有,即沒有實質鏈接上 false true
顯示鏈接已保存,標題欄也有已鏈接上的圖標 true true
選擇不保存後 false true
選擇鏈接,在正在獲取 IP 地址時 false false

網絡鏈接類型

經過 NetworkInfo 中的 getNetworkType() 方法能夠獲取當前網絡類型。

public static final int NETWORK_NONE = 0;
public static final int NETWORK_WIFI = 1;
public static final int NETWORK_MOBILE = 10;
public static final int NETWORK_2G = 12;
public static final int NETWORK_3G = 13;
public static final int NETWORK_4G = 14;
/**
 * 獲取當前的網絡狀態
 *
 * @param context
 * @return 沒有網絡:0; WIFI:1; 手機網絡:10; 2G:12; 3G:13; 4G:14;
 */
public static int getNetworkType(Context context) {
  ConnectivityManager connectivityManager = (ConnectivityManager)
                            context.getSystemService(Context.CONNECTIVITY_SERVICE);
  if (connectivityManager == null) return NETWORK_NONE;

  NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
  if (networkInfo == null || !networkInfo.isAvailable())
    return NETWORK_NONE;

  int type = networkInfo.getType();
  if (type == ConnectivityManager.TYPE_WIFI) {
    return NETWORK_WIFI; // WiFi
  }

  if (type == ConnectivityManager.TYPE_MOBILE) {
    TelephonyManager telephonyManager = (TelephonyManager)
                          context.getSystemService(Context.TELEPHONY_SERVICE);
    if (telephonyManager == null) return NETWORK_NONE;

    int networkType = telephonyManager.getNetworkType();
    switch (networkType) {
        // 2G
      case TelephonyManager.NETWORK_TYPE_GPRS:
      case TelephonyManager.NETWORK_TYPE_CDMA:
      case TelephonyManager.NETWORK_TYPE_EDGE:
      case TelephonyManager.NETWORK_TYPE_1xRTT:
      case TelephonyManager.NETWORK_TYPE_IDEN:
        return NETWORK_2G;
        // 3G
      case TelephonyManager.NETWORK_TYPE_EVDO_A:
      case TelephonyManager.NETWORK_TYPE_UMTS:
      case TelephonyManager.NETWORK_TYPE_EVDO_0:
      case TelephonyManager.NETWORK_TYPE_HSDPA:
      case TelephonyManager.NETWORK_TYPE_HSUPA:
      case TelephonyManager.NETWORK_TYPE_HSPA:
      case TelephonyManager.NETWORK_TYPE_EVDO_B:
      case TelephonyManager.NETWORK_TYPE_EHRPD:
      case TelephonyManager.NETWORK_TYPE_HSPAP:
        return NETWORK_3G;
        // 4G
      case TelephonyManager.NETWORK_TYPE_LTE:
        return NETWORK_4G;
      default:
        return NETWORK_MOBILE;
    }
  }
  return NETWORK_NONE;
}

監聽網絡變化

首先,建立一個 NetworkReceiver。

public class NetworkReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    Log.d("NetworkReceiver", "網絡發生變化");
    String action = intent.getAction();
    if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
      int networkType = NetworkUtil.getNetworkType(context);
      Log.e("NetworkReceiver", "networkType = " + networkType);
      Toast.makeText(context, "當前網絡:" + networkType,
                     Toast.LENGTH_SHORT).show();
    }
  }
}

而後,在 AndroidManifest.xml 文件中註冊 NetworkReceiver。

<receiver android:name=".ui.broadcast.NetworkReceiver">
  <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    <action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
    <action android:name="android.net.wifi.STATE_CHANGE" />
  </intent-filter>
</receiver>

並添加監聽網絡須要的相關權限。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
在 Android 7.0 以後靜態註冊廣播的方式被取消了,因此咱們這裏還須要採用動態註冊的方

在 Android 7.0 以後靜態註冊廣播的方式被取消了,因此咱們這裏還須要採用動態註冊的方式。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  NetworkReceiver networkReceiver = new NetworkReceiver();
  IntentFilter filter = new IntentFilter();
  filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
  filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
  filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
  registerReceiver(networkReceiver, filter);
}
設置網絡緩存

設置網絡緩存

在必定時間內,對服務端返回的數據進行緩存,好比一些接口的數據不會更新(10 分鐘或更久變化一次),咱們就能夠緩存該接口的數據,設定有效時間,能夠減小沒必要要的流量消耗。

Android 系統上關於網絡請求的 Http Response Cache 是默認關閉的,這樣會致使每次即便請求的數據內容是同樣的也會須要重複被調用執行,效率低下。

咱們能夠經過下面的代碼示例開啓 HttpResponseCache。

protected void onCreate(Bundle savedInstanceState) {
  // ...
  try {
    File httpCacheDir = new File(context.getCacheDir(), "http");
    long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
    HttpResponseCache.install(httpCacheDir, httpCacheSize);
  } catch (IOException e) {
    Log.i(TAG, "HTTP response cache installation failed:" + e);
  }
}

protected void onStop() {
  // ...
  HttpResponseCache cache = HttpResponseCache.getInstalled();
  if (cache != null) {
    cache.flush();
  }
}

開啓 Http Response Cache 以後,Http 操做相關的返回數據就會緩存到文件系統上,不只僅是主程序本身編寫的網絡請求相關的數據會被緩存,另外引入的 library 庫中的網絡相關的請求數據也會被緩存到這個 Cache 中。

備註:若是所有本身從頭開始寫會比較繁瑣複雜,有很多著名的開源框架 Volley、Okhttp 都很好的支持實現自定義緩存。

減少傳輸數據量

爲了可以減少網絡傳輸的數據量,咱們須要對傳輸的數據作壓縮的處理,這樣可以提升網絡操做的性能。首先不一樣的網絡環境,下載速度以及網絡延遲是存在差別的,以下圖所示:asset load

若是咱們選擇在網速更低的網絡環境下進行數據傳輸,這就意味着須要執行更長的時間,而更長的網絡操做行爲,會致使電量消耗更加嚴重。另外傳輸的數據若是不作壓縮處理,也一樣會增長網絡傳輸的時間,消耗更多的電量。不只如此,未通過壓縮的數據,也會消耗更多的流量,使得用戶須要付出更多的流量費。

一般來講,網絡傳輸數據量的大小主要由兩部分組成:圖片與序列化的數據,那麼咱們須要作的就是減小這兩部分的數據傳輸大小,分下面兩個方面來討論。

  • 使用不一樣分辨率的圖片

首先須要作的是減小圖片的大小,選擇合適的圖片保存格式是第一步。下圖展現了 PNG、JPEG、WEBP 三種主流格式在佔用空間與圖片質量之間的對比:png jpg webp

對於 JPEG 與 WEBP 格式的圖片,不一樣的清晰度對佔用空間的大小也會產生很大的影響,適當的減小 JPG 質量,能夠大大的縮小圖片佔用的空間大小。

另外,咱們須要爲不一樣的使用場景提供當前場景下最合適的圖片大小,例如針對全屏顯示的狀況咱們會須要一張清晰度比較高的圖片,而若是隻是顯示爲縮略圖的形式,就只須要服務器提供一個相對清晰度低不少的圖片便可。

服務器應該支持到爲不一樣的使用場景分別準備多套清晰度不同的圖片,以便在對應的場景下可以獲取到最適合本身的圖片。這雖然會增長服務端的工做量,但是這個付出卻十分值得!

  • 壓縮序列化數據

其次須要作的是減小序列化數據的大小,不直接使用 JSON 和 XML 格式數據。

JSON 與 XML 爲了提升可讀性,在文件中加入了大量的符號,空格等等字符,而這些字符對於程序來講是沒有任何意義的。咱們應該使用 Protocal Buffers,Nano-Proto-Buffers,FlatBuffer 來減少序列化的數據的大小。

Protocol Buffer 是 Google 開發的一種數據交換的格式,它獨立於語言,獨立於平臺。相較於目前經常使用的 JSON,數據量更小,意味着傳輸速度也更快。

IP 直連與 DNS

DNS 解析的失敗率佔聯網失敗中很大一種,並且首次域名解析通常須要幾百毫秒。針對此,咱們能夠不用域名,採用 IP 直連省去 DNS 解析過程,節省這部分時間。

另外熟悉阿里雲的小夥伴確定知道 HTTPDNS,HTTPDNS 基於 HTTP 協議的域名解析,替代了基於 DNS 協議向運營商 Local DNS 發起解析請求的傳統方式,能夠避免 Local DNS 形成的域名劫持和跨網訪問問題,解決域名解析異常帶來的困擾。

文件下載與上傳

文件、圖片等的下載,採用斷點續傳,不浪費用戶以前消耗過的流量。

文件的上傳失敗率比較高,不只僅由於大文件,同時帶寬、時延、穩定性等因素在此場景下的影響也更加明顯。

  • 避免整文件傳輸,採用分片傳輸;
  • 根據網絡類型以及傳輸過程當中的變化動態的修改分片大小;
  • 每一個分片失敗重傳的機會。

HTTP 協議優化

使用最新的協議,HTTP 協議有多個版本:0.九、1.0、1.一、2 等。

新版本的協議通過再次的優化,例如:

  • HTTP 1.1 版本引入了「持久鏈接」,多個請求被複用,無需重建 TCP 鏈接,而 TCP 鏈接在移動互聯網的場景下成本很高,節省了時間與資源。
  • HTTP 2 引入了「多工」、頭信息壓縮、服務器推送等特性。

新的版本不只能夠節省資源,一樣能夠減小流量。

請求打包

合併網絡請求,減小請求次數。對於一些接口類如統計,無需實時上報,將統計信息保存在本地,而後根據策略統一上傳。這樣頭信息僅需上傳一次,減小了流量也節省了資源。

Network Monitor

Network Profiler 是 Android Profiler 中的一個組件,可幫助開發者識別致使應用卡頓、OOM 和內存泄露。它顯示一個應用內存使用量的實時圖表,能夠捕獲堆轉儲、強制執行垃圾回收以及跟蹤內存分配。

能夠在 View > Tool Windows > Android Profiler 中打開 Network Profiler 界面。![Network Monitor
](https://upload-images.jianshu...

Network Profiler 的具體使用可查看 Android 開發文檔 - 利用 Network Profiler 檢查網絡流量、Android Studio 3.0 利用 Android Profiler 測量應用性能 這兩篇文章。

抓包工具

使用 Charles、Fiddler 等抓包工具一樣能夠實現 Network Monitor 的功能,並且更增強大。

Stetho

Stetho 是 Facebook 出品的一個 Android 應用的調試工具。無需 Root 便可經過 Chrome,在 Chrome Developer Tools 中可視化查看應用佈局、網絡請求、SQLite,Preference 等。一樣集成了 Stetho 以後也能夠很方便的查看網絡請求的各類狀況。Network Inspection

阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題▲15G的Android架構進階、視頻資料及安卓程序員簡歷模板

相關文章
相關標籤/搜索