1.前言
提到OKHttp大家都不陌生,OKHttp中的拦截器也在大家的项目中或多或少的被使用到,通常我们的使用是这样的
OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new LoggingInterceptor()) .addNetworkInterceptor(new TokenInterceptor()) .build();
使用到应用拦截器(addInterceptor)和网络拦截器(addNetworkInterceptor)。
2. OKHTTP中的拦截器
拦截器是一个非常powerful的机制,包括js领域中的axios等主流网络请求框架都引入了类似的机制。而OKHTTP可以说是网络请求拦截器的始祖。
在OKHTTP中,拦截器主要通过chain.processd(request)来实现功能,这个简单的方法是http工作发生,产生满足请求的响应之处。
在使用中,我们可以将拦截器注册为应用拦截器和网络拦截器。
接下来使用官网的示例来看一看两者的不同。
首先编写一个日志请求的拦截器LoggingInterceptor
class LoggingInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); logger.info(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); logger.info(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); return response; }}
很简单的功能,就是打印网络请求。
接下来我们分别把它注册为应用拦截器和网络拦截器看看有什么不同
应用拦截器的LoggingInterceptor
OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new LoggingInterceptor()) .build();Request request = new Request.Builder() .url("http://www.publicobject.com/helloworld.txt") .header("User-Agent", "OkHttp Example") .build();Response response = client.newCall(request).execute();response.body().close();
打印出来的日志是这样的
INFO: Sending request http://www.publicobject.com/helloworld.txt on nullUser-Agent: OkHttp ExampleINFO: Received response for https://publicobject.com/helloworld.txt in 1179.7msServer: nginx/1.4.6 (Ubuntu)Content-Type: text/plainContent-Length: 1759Connection: keep-alive
可以看到日志信息比较简单,只有基础的请求信息。
接下来我们再把它注册为网络拦截器
OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new LoggingInterceptor()) .build();Request request = new Request.Builder() .url("http://www.publicobject.com/helloworld.txt") .header("User-Agent", "OkHttp Example") .build();Response response = client.newCall(request).execute();response.body().close();
打印出来的日志是这样的
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}User-Agent: OkHttp ExampleHost: www.publicobject.comConnection: Keep-AliveAccept-Encoding: gzipINFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6msServer: nginx/1.4.6 (Ubuntu)Content-Type: text/htmlContent-Length: 193Connection: keep-aliveLocation: https://publicobject.com/helloworld.txtINFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}User-Agent: OkHttp ExampleHost: publicobject.comConnection: Keep-AliveAccept-Encoding: gzipINFO: Received response for https://publicobject.com/helloworld.txt in 80.9msServer: nginx/1.4.6 (Ubuntu)Content-Type: text/plainContent-Length: 1759Connection: keep-alive
很明显包含的信息更多了。
对比信息后可以看到这个请求实际上进行了重定向,而重定向的过程在使用应用拦截器时并没有被打印出来,但是在使用网络拦截器时被打印出来了。
此外,网络拦截器的Chain有一个非空的Connection,可以用来访问IP地址和用来连接网络服务器的TLS配置等信息。
那么我们在使用时该如何选择呢?
每种拦截器chain有相对的优势。 应用拦截器
- 不需要关心像重定向和重试这样的中间响应。
- 总是调用一次,即使HTTP响应从缓存中获取服务。
- 监视应用原始意图。不关心OkHttp注入的像If-None-Match头。
- 允许短路并不调用Chain.proceed()。
- 允许重试并执行多个Chain.proceed()调用。
网络拦截器
- 可以操作像重定向和重试这样的中间响应。
- 对于短路网络的缓存响应不会调用。
- 监视即将要通过网络传输的数据。
- 访问运输请求的Connection。
3. OKHTTP源码中的拦截器使用
在OKHTTP的源码中我找到如下代码
@Throws(IOException::class) internal fun getResponseWithInterceptorChain(): Response { // Build a full stack of interceptors. val interceptors = mutableListOf<Interceptor>() interceptors += client.interceptors interceptors += RetryAndFollowUpInterceptor(client) interceptors += BridgeInterceptor(client.cookieJar) interceptors += CacheInterceptor(client.cache) interceptors += ConnectInterceptor if (!forWebSocket) { interceptors += client.networkInterceptors } interceptors += CallServerInterceptor(forWebSocket) val chain = RealInterceptorChain( call = this, interceptors = interceptors, index = 0, exchange = null, request = originalRequest, connectTimeoutMillis = client.connectTimeoutMillis, readTimeoutMillis = client.readTimeoutMillis, writeTimeoutMillis = client.writeTimeoutMillis ) var calledNoMoreExchanges = false try { val response = chain.proceed(originalRequest) if (isCanceled()) { response.closeQuietly() throw IOException("Canceled") } return response } catch (e: IOException) { calledNoMoreExchanges = true throw noMoreExchanges(e) as Throwable } finally { if (!calledNoMoreExchanges) { noMoreExchanges(null) } } }
代码文件在main/kotlin/okhttp3/internal/connection/RealCall.kt
通过这个方法我们可以看出拦截器在okhttp内部也是被大量使用的
- addInterceptor(Interceptor),这是由开发者设置的,会按照开发者的要求,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。
- RetryAndFollowUpInterceptor,这里会对连接做一些初始化工作,以及请求失败的充实工作,重定向的后续请求工作。跟他的名字一样,就是做重试工作还有一些连接跟踪工作。
- BridgeInterceptor,这里会为用户构建一个能够进行网络访问的请求,同时后续工作将网络请求回来的响应Response转化为用户可用的Response,比如添加文件类型,content-length计算添加,gzip解包。
- CacheInterceptor,这里主要是处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
- ConnectInterceptor,这里主要就是负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCodec
- networkInterceptors,这里也是开发者自己设置的,所以本质上和第一个拦截器差不多,但是由于位置不同,所以用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。
- CallServerInterceptor,这里就是进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。
OkHttp拦截器的实现
OkHttp中的拦截器实现,这里做简单记录:
- 拦截器的接口定义如下:
编辑切换为居中
添加图片注释,不超过 140 字(可选)
可以看到,在拦截器的回调中,我们可以拿到两个重要的参数Request和Response,而接口在回调时,会接收一个Chain类型的参数,这个参数保存了Request和Response的相关数据。
- 看一个简单的Interceptor接口实现例子:
编辑切换为居中
添加图片注释,不超过 140 字(可选)
该拦截器实例的功能只是在请求发出前和接收到响应后,分别打印log。Response response=chain.proceed(request); 很重要,它是将拦截连串起来的关键。
- 再看下,一个网络请求的应用吧:
跟进,OkHttpClient.Builder的addInterceptor()方法,可以看到,在OkHttp内部是使用了List保存了添加的所有拦截器。
- 跟进client.newCall(request).execute();的执行过程,查看拦截器的串联过程:
从上面的方法中可以看到,请求默认会被构造成RealCall类型,再进一步查看RealCall的execute()方法:
上面的红色方框中这行代码,它并没有真正的执行网络请求,而只是简单地将请求放入请求池中,让它等待分派器的后续执行。而真正的执行体在上面的蓝色方框中,它封装了一个拦截器链(Chain),并调用了Chain的proced方法,传入原始的request对象,这里开始拦截器链的调用过程:
- 查看proceed方法,可以看到,它使用循环+递归的方式,借助函数调用栈,将拦截器串联起来:
上面的代码中,真正执行网络请求的是最后一行绿色方框中的代码,而当存在多个拦截器时,每个拦截器在执行时都会在上图的蓝色方框中的代码地方阻塞,等待下一个拦截器的调用返回。
- 为了说明清楚,下面分别以 拦截器链中有1个、2个拦截器的场景加以模拟:
以上就是Android开发中的OKhttp的拦截原理及实现;更多Android的精通学习可以参考《Android核心技术手册》进行追击进阶自己。