一、透明压缩
OkHttp支持透明压缩
。在构建请求时,我们无需设置请求头Accept-Encoding
,在BridgeInterceptor.intercept()
方法中会主动添加请求头Accept-Encoding:gzip
。 布尔值transparentGzip
只有在我们没设置Accept-Encoding
且Range
为null(即不是分批下载或断点续传)时,才会为true。进而当响应带有Content-Encoding:gzip
时,OkHttp将自动帮我们解压数据,并移除Content-Encoding
响应头(避免调用方再次解压响应体 )。 可见,如果我们主动添加了Accept-Encoding:gzip
请求头,transparentGzip=false,OkHttp将不会尝试解压响应体,需要我们自己处理。
二、OkHttp调优
2.1 Interceptors和NetworkInterceptors区别
OkHttpClient.Builder可以通过addInterceptor和addNetworkdInterceptor添加自定义的拦截器。虽然NetworkdInterceptor也是指Interceptor
接口实例,当两者作用范围有区别。
- Interceptors位于拦截器链的头部,用于观察HTTP请求的整个过程,从连接建立到获得响应。
- NetworkdInterceptors位于
CallServerInterceptor
之前,仅用于观察网络I/O阶段。 可见,在请求重试或重定向时,Interceptors不会被再次执行,而NetworkdInterceptors将被再次执行。
2.2 复用OkhttpClient
反例
如下面代码,某个公司对Okhttp进行封装,每发起一个HTTP请求,都会创建一个ThirdHttpInvoker
实例,进而新建一个OkhttpClient
实例。 这儿创建OkhttpClient
实例时,没有主动提供ConnectionPool
,将由框架创建一个新的连接池,而每个连接池都有一个cleanup线程。 当程序对外HTTP请求并发高时,将频繁创建没有复用的OkhttpClient
实例、连接池、Dispatcher异步请求线程池等,浪费资源;且同一域名连接不能复用,严重降低了Okhttp性能。
正确做法
使用Okhttp时,OkhttpClient
应当是全局单列。还可以自己配置并创建单例的ConnectionPool、Dispatcher。
java
public static final ConnectionPool POOL = new ConnectionPool(256, 5, TimeUnit.MINUTES);
public static final Dispatcher DISPATCHER = new Dispatcher();
static {
// AsyncCall并发度,默认为64
DISPATCHER.setMaxRequests(256);
// 对每个域名AsyncCall并发度,默认为5
DISPATCHER.setMaxRequestsPerHost(32);
// Dispatcher空闲时的回调逻辑
DISPATCHER.setIdleCallback(() -> {
// do something
});
}
public static final OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(POOL)
.dispatcher(DISPATCHER)
.connectTimeout(5L, TimeUnit.SECONDS)
.readTimeout(20L, TimeUnit.SECONDS)
.build();
2.3 并发度调整
Dispatcher类有两个属性,限制了OkhttpClient实例异步请求最大并发数、对每个Host异步请求的最大并发数。超过限制时,请求需要在队列中排队依次等候。 并发度默认值较低,我们应当根据实际场景进行调整。
java
// 异步请求最大并发数
private int maxRequests = 64;
// 每个Host异步请求最大并发数
private int maxRequestsPerHost = 5;
2.4 自定义请求超时时间
设置全局超时时间
在创建OkHttpClient时,我们可以设置3个超时时间(默认都是10秒),很显然这是全局设置。
java
public static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5L, TimeUnit.SECONDS)
.writeTimeout(20L, TimeUnit.SECONDS)
.readTimeout(20L, TimeUnit.SECONDS)
.build();
处理每个请求时会创建Interceptor.Chain
,将超时配置传递给了拦截器链。 当执行ConnectInterceptor
拦截逻辑时,ExchangeFinder#findConnection中创建Socket连接,使用了超时配置。
- writeTimeout在使用https时才会生效,对http请求writeTimeout会被忽略;
- connectTimeout、readTimeout是Socket的方法参数。
java
// java.net.Socket中方法
Socket.connect(address, connectTimeout);
Socket.setSoTimeout(readTimeout);
而真实场景中,不同请求对超时时间可能会有不同要求,如何设置请求级别超时时间呢?
设置请求级别超时
超时时间是通过接口Chain
向下传递,而该接口提供了3个超时时间的赋值器。那么,我们可以自定义一个Interceptor
,将通过请求头声明的超时时间设置给Chain
,来覆盖全局的超时设置。 例如RequestTimeoutInterceptor
实现,在创建OkHttpClient
时引用它。
java
public static final OkHttpClient client = new OkHttpClient.Builder()
// 全局默认超时设置
.connectTimeout(5L, TimeUnit.SECONDS)
.writeTimeout(20L, TimeUnit.SECONDS)
.readTimeout(20L, TimeUnit.SECONDS)
// 处理请求级别超时设置
.addInterceptor(new RequestTimeoutInterceptor())
.build();
java
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* 请求级别超时时间拦截器
*/
public class RequestTimeoutInterceptor implements Interceptor {
// 超时时间请求头,单位毫秒
public static final String CONNECT_TIMEOUT = "connectTimeout";
public static final String WRITE_TIMEOUT = "writeTimeout";
public static final String READ_TIMEOUT = "readTimeout";
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request.Builder builder = request.newBuilder();
String connectTimeoutStr = request.header(CONNECT_TIMEOUT);
// 请求没有声明超时时间,则使用全局配置
int connectTimeout = chain.connectTimeoutMillis();
if (Objects.nonNull(connectTimeoutStr)) {
connectTimeout = Integer.parseInt(connectTimeoutStr);
builder.removeHeader(CONNECT_TIMEOUT);
}
String writeTimeoutStr = request.header(WRITE_TIMEOUT);
int writeTimeout = chain.writeTimeoutMillis();
if (Objects.nonNull(writeTimeoutStr)) {
writeTimeout = Integer.parseInt(writeTimeoutStr);
builder.removeHeader(WRITE_TIMEOUT);
}
String readTimeoutStr = request.header(READ_TIMEOUT);
int readTimeout = chain.readTimeoutMillis();
if (Objects.nonNull(readTimeoutStr)) {
readTimeout = Integer.parseInt(readTimeoutStr);
builder.removeHeader(READ_TIMEOUT);
}
return chain.withConnectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.withWriteTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.withReadTimeout(readTimeout, TimeUnit.MILLISECONDS)
.proceed(builder.build());
}
}
创建Request
对象时,添加超时时间请求头。
java
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.addHeader(RequestTimeoutInterceptor.CONNECT_TIMEOUT, "3000")
.addHeader(RequestTimeoutInterceptor.WRITE_TIMEOUT, "10000")
.addHeader(RequestTimeoutInterceptor.READ_TIMEOUT, "10000")
.build();