如何更高效地使用 OkHttp

jcodecraeer發表於2016-02-23

在為 可汗學院 開發  Android app 時, OKHttp 是一個很重要的開源庫。雖然它的預設配置已經提供了很好的效果,但是我們還是採取了一些措施提高 OkHttp 的可用性和自我檢查能力:

1. 在檔案系統中開啟響應快取

有些響應訊息通過包含 Cache-Control HTTP 首部欄位允許快取,但是預設情況下,OkHttp 並不會快取這些響應訊息。因此你的客戶端可能會因為不斷請求相同的資源而浪費時間和頻寬,而不是簡單地讀取一下首次響應訊息的快取副本。

為了在檔案系統中開啟響應快取,需要配置一個 com.squareup.okhttp.Cache 例項,然後把它傳遞給 OkHttpClient 例項的 setCache 方法。你必須用一個表示目錄的 File 物件和最大位元組數來例項化 Cache 物件。那些能夠快取的響應訊息會被寫在指定的目錄中。如果已快取的響應訊息導致目錄內容超過了指定的大小,響應訊息會按照最近最少使用( LRU Policy )的策略被移除。

正如 Jesse Wilson 所建議的 ,我們將響應訊息快取在 context.getCacheDir() 的子資料夾中:

// 快取根目錄,由這裡推薦 -> http://stackoverflow.com/a/32752861/400717.
// 小心可能為空,參考下面兩個連結
// https://groups.google.com/d/msg/android-developers/-694j87eXVU/YYs4b6kextwJ 和
// http://stackoverflow.com/q/4441849/400717.
final @Nullable File baseDir = context.getCacheDir();
if (baseDir != null) {
  final File cacheDir = new File(baseDir, "HttpResponseCache");
  okHttpClient.setCache(new Cache(cacheDir, HTTP_RESPONSE_DISK_CACHE_MAX_SIZE));
}

在可汗學院的應用中,我們指定了 HTTP_RESPONSE_DISK_CACHE_MAX_SIZE 的大小為  10 * 1024 * 1024 ,即 10MB。

2. 整合 Stetho

Stetho 是一個 Facebook 出品的超讚的開源庫,它可以讓你用 Chrome 的功能—— 開發者工具 來檢查除錯你的 Android 應用。

Stetho 不僅能夠檢查應用的 SQLite 資料庫和檢視層次,還可以檢查 OkHttp 的每一條請求和響應訊息:

如何更高效地使用 OkHttp

這種自我檢查方式(Introspection)有效地確保了伺服器返回允許快取資源的 HTTP 首部時,且核快取資源存在時,不再發出任何請求。

開啟 Stetho,只用簡單地新增一個 StethoInterceptor 例項到網路攔截器(Network Interceptor)的列表中去:

okHttpClient.networkInterceptors().add(new StethoInterceptor());

應用執行完畢之後,開啟 Chrome 然後跳轉到 chrome://inspect。裝置、應用以及應用識別符號資訊會被陳列出來。直接點選“inspect”連結就可以開啟開發者工具,然後切換到 Network 標籤開始監測 OkHttp 發出的請求。

3. 使用 Picasso 和 Retrofit

可能和我們一樣,你使用 Picasso 來載入網路圖片,或者使用  Retrofit 來簡化網路請求和解析響應訊息。在預設情況下,如果你沒有顯式地指定一個 OkHttpClient,這些開源庫會隱式地建立它們自己的 OkHttpClient 例項以供內部使用。以下程式碼來自於 Picasso 2.5.2 版本的OkHttpDownloader 類:

private static OkHttpClient defaultOkHttpClient() {
  OkHttpClient client = new OkHttpClient();
  client.setConnectTimeout(Utils.DEFAULT_CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
  client.setReadTimeout(Utils.DEFAULT_READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
  client.setWriteTimeout(Utils.DEFAULT_WRITE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
  return client;
}

Retrofit 也有類似的工廠方法用來建立它自己的 OkHttpClient。

圖片是應用中需要載入的最大的資源之一。Picasso 是嚴格地按照 LRU 策略在記憶體中維護它的圖片快取。如果客戶端嘗試用 Picasso 載入一張圖片,並且 Picasso 沒有在記憶體快取中找到該圖片,那麼它會委託內部的 OkHttpClient 例項來載入該圖片。在預設情況下,由於前面的defaultOkHttpClient 方法沒有在檔案系統中配置響應快取,該例項會一直從伺服器載入圖片。

自定義一個 OkHttpClient 例項,將從檔案系統返回一個已快取的響應訊息這種情況考慮在內。沒有一張圖片直接從伺服器載入。這在應用第一次載入時是尤為重要的。在這個時候,Picasso 的記憶體中的快取是 “冷” 的,它會頻繁地委託 OkHttpClient 例項去載入圖片。

這就需要構建一個用你的 OkHttpClient 配置的 Picasso 例項。如果你在程式碼中使用Picasso.with(context).load(…) 來載入圖片,你所使用的 Picasso 單例物件,是在with 方法中用自己的 OkHttpClient 延遲載入和配置的。因此我們必須在第一次呼叫 with方法之前指定自己的 Picasso 例項作為單例物件。

簡單地把 OkHttpClient 例項包裝到一個 OkHttpDownloader 物件中,然後傳遞給Picasso.Builder 例項的 downloader 方法:

final Picasso picasso = new Picasso.Builder(context)
    .downloader(new OkHttpDownloader(okHttpClient))
    .build();

//客戶端應該在任何需要的時候來建立這個例項
//以防萬一,替換掉那個單例物件
Picasso.setSingletonInstance(picasso);

在 Retrofit 1.9.x 中,通過 RestAdapter 使用你的 OkHttpClient 例項,把 OkHttpClient例項包裝到一個 OkClient 例項中,然後傳遞給 RestAdapter.Builder 例項的 setClient方法:

restAdapterBuilder.setClient(new OkClient(httpClient));

在 Retrofit 2.0 中,直接把 OkHttpClient 例項傳遞給 Retrofit.Builder 例項的 client即可。

在可汗學院的應用中,我們使用 Dagger 來確保只有一個 OkHttpClient 例項,而且 Picasso 和 Retrofit 都會使用到它。我們為帶 @Singleton 註解的 OkHttpClient 例項建立了一個 provider:

@Provides
@Singleton
public OkHttpClient okHttpClient(final Context context, ...) {
  final OkHttpClient okHttpClient = new OkHttpClient();
  configureClient(okHttpClient, ...);
  return okHttpClient;
}

這個 OkHttpClient 例項隨後通過 Dagger 注入到其他用來建立 RestAdapter 和 Picasso例項的 provider 裡。

4. 設定使用者代理攔截器(User-Agent Interceptor)

當客戶端在每一次請求中都提供一個詳細的 User-Agent 頭部資訊時,日誌檔案和分析資料提供了很有用的資訊。預設情況下,OkHttp 的 User-Agent 值僅僅只有它的版本號。要設定你自己的 User-Agent,建立一個攔截器(Interceptor)然後替換掉預設值,參考 StackOverflow 上的建議

public final class UserAgentInterceptor implements Interceptor {
  private static final String USER_AGENT_HEADER_NAME = "User-Agent";
  private final String userAgentHeaderValue;

  public UserAgentInterceptor(String userAgentHeaderValue) {
    this.userAgentHeaderValue = Preconditions.checkNotNull(userAgentHeaderValue);
  }

  @Override
  public Response intercept(Chain chain) throws IOException {
    final Request originalRequest = chain.request();
    final Request requestWithUserAgent = originalRequest.newBuilder()
        .removeHeader(USER_AGENT_HEADER_NAME)
        .addHeader(USER_AGENT_HEADER_NAME, userAgentHeaderValue)
        .build();
    return chain.proceed(requestWithUserAgent);
  }
}

使用任何你覺得有價值的資訊,來建立 User-Agent 值,然後傳遞給UserAgentInterceptor 的建構函式。我們使用了這些欄位:

  • os 欄位,值設定為 Android,明確表明這是一個 Android 裝置
  • Build.MODEL 欄位,即使用者可見的終端產品的名稱
  • Build.BRAND 欄位,即消費者可見的跟產品或硬體相關的商標
  • Build.VERSION.SDK_INT 欄位,即使用者可見的 [Android] 框架版本號
  • BuildConfig.APPLICATION_ID 欄位
  • BuildConfig.VERSION_NAME 欄位
  • BuildConfig.VERSION_CODE欄位

最後三個欄位是根據我們的 Gradle 構建指令碼中的 applicationId, versionCode 和versionName 的值來確定的。瞭解更多資訊請參考文件 應用版本控制 ,和  使用 Gradle 配置你的 applicationId

小提示:如果你的應用中用到了 WebView,你可以配置使用相同的 User-Agent 值,即之前建立的 UserAgentInterceptor:

WebSettings settings = webView.getSettings();
settings.setUserAgentString(userAgentHeaderValue);

5. 指定合理的超時

在 2.5.0 版本之前,OkHttp 請求預設永不超時。從 2.5.0 版本開始,如果建立了一個連線,或從連線讀取下一個位元組,或者向連線寫入下一個位元組,用時超過了10秒,請求就會超時。分別呼叫 setConnectTimeout,setReadTimeout 或 setWriteTimeout 方法可以重寫那些預設值。

小提示:Picasso 和 Retrofit 為它們的預設 OkHttpClient 例項指定不同的超時時長。 預設情況下, Picasso 設定如下:

  • 連線超時15秒
  • 讀取超時20秒
  • 寫入超時20秒

Retrofit 設定如下:

  • 連線超時15秒
  • 讀取超時20秒
  • 寫入無超時

用你自己的 OkHttpClient 例項配置好 Picasso 和 Retrofit 之後,就能確保所有請求超時的一致性了。

結論

再次強調,OkHttp 的預設配置提供了顯著的效果,但是採取以上的措施,可以提高 OkHttp 的可用性和自我檢查能力,並且提升你的應用的質量。

相關文章