[译] 如何更高效地使用 OkHttp

11,307 阅读7分钟
原文链接: blog.bruce3x.com

在为可汗学院开发 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 的每一条请求和响应消息:

Image of Stetho

这种自我检查方式(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 注入到其他用来创建 RestAdapterPicasso 实例的 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, versionCodeversionName 的值来确定的。了解更多信息请参考文档 应用版本控制,和 使用 Gradle 配置你的 applicationId

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

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

5. 指定合理的超时

在 2.5.0 版本之前,OkHttp 请求默认永不超时。从 2.5.0 版本开始,如果建立了一个连接,或从连接读取下一个字节,或者向连接写入下一个字节,用时超过了10秒,请求就会超时。分别调用 setConnectTimeoutsetReadTimeoutsetWriteTimeout 方法可以重写那些默认值。

小提示:Picasso 和 Retrofit 为它们的默认 OkHttpClient 实例指定不同的超时时长。 默认情况下, Picasso 设定如下:

  • 连接超时15秒
  • 读取超时20秒
  • 写入超时20秒

Retrofit 设定如下:

  • 连接超时15秒
  • 读取超时20秒
  • 写入无超时

用你自己的 OkHttpClient 实例配置好 Picasso 和 Retrofit 之后,就能确保所有请求超时的一致性了。

结论

再次强调,OkHttp 的默认配置提供了显著的效果,但是采取以上的措施,可以提高 OkHttp 的可用性和自我检查能力,并且提升你的应用的质量。