[开源学习笔记][Android][OkHttp]Interceptor原理解析和实践应用

813 阅读6分钟

摘要

OkHttp 中的 Interceptor 机制是其特性之一,为外部扩展定制提供了良好的 API 支持。本文通过深入源码,绘制流程图等方式详细讲解 Interceptor 的实现思路,对比分析 Interceptor 的使用场景和优势劣势,并应用同样原理实现真实项目的需求作为实践部分的示例代码。最后扩展部分提供了一部分个人关于编程思路的理解和思考,不一定对,欢迎探讨。

关键词:OkHttp源码 | Interceptor | 编程思维 | Android

正文

通过拦截器实现对流程的解耦

Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls.

OkHttp 的文档中单独有一个分标题是 Interceptors,这是 Okhttp 对 Interceptors 的介绍,Interceptors 的功能有「监测」「重写」和「重试」。使用方法也很简单,创建一个类实现 Interceptor 接口,再添加到 OkHttpClient 中即可。

Interceptor 可以获取到一个当前的 request 和 response,实现「监测」;在此基础上,每个 Interceptor 可以修改传给下一个 Interceptor 的 request 和返回给上一个 Interceptor 的 response,实现对数据的「重写」;「重试」相对复杂一点,在 Interceptor 拿到 response(或者Exception时)判断 response 是否正确,出现错误时再次执行 chain.proceed(request) 就实现了「重试」。

实现上述功能的工作流程也不复杂,文档中有一个示例图:

一个 request 到 response 的过程经过了层层 Interceptor,我把这部分结合具体代码画了一个更详细的图:

多个 Interceptor 构成一个调用栈,每个 Interceptor 可以在执行 chain.proceed 之前修改 request,在 return response 之前修改 response。整体结构有些像递归调用,只是每个Interceptor调用的不是自身的 intercept 方法,而是下一个 Interceptor 的 intercept 方法。

具体的实现是在 RealInterceptorChain 中持有 interceptors 数组和当前的 index,执行 proceed 的时候取 index+1 的 Interceptor,再调用其 intercept 方法。

OkHttp 用 Kotlin 重构之后应该是比较优秀的 Kotlin 编写 lib 的教程了,不仅应用了很多 Kotlin 语言特性,还为保持 Java 调用时的 API 风格做了很多额外处理,强烈推荐学习一下。

使用场景分析

在一个流程中暴露出接口,给外部提供控制流程的方法是编程中经常出现的,在不同的情况下可以有不同的解决方案。Interceptor 机制是一种支持高度定制的流程控制解决方案,适合用在比较复杂的情况中。

对流程进行控制的解决方案还有其他的,比如简单的在触发点修改输入或输出数据,在调用流程中返回 boolean 值提前终止流程等。Android 中 View 的事件分发模型也是一种对流程的

Interceptor 的优点是功能全面,灵活度高,在有多个 Interceptor 时代码结构依然清晰;缺点是对外部注入的 Interceptor 缺乏限制,可能破坏已有结构,需要开发人员理解整个流程,上手难度略高。

实践:给 WebView 加载流程添加一个拦截器

假设有一个这样的需求,我们需要自定义一个 WebView 作为运营活动的 Web 页面的容器,其中需要实现两个拦截 url 的功能,一是对符合某些规则的 url 进行替换,显示对不同用户的定制页面;二是页面中的音频可以复用本地的 DiskLRUCache,提高音频的流畅度,减少数据流量消耗。

简单分析一下需求,两个功能分别是在拦截时修改输入 url 和在拦截时修改输出的 response,如果都写在 shouldInterceptRequest 中,就需要复杂的 if 判断逻辑,每次新增一种拦截规则都需要修改 shouldInterceptRequest 处的代码。 加载网页和发起网络请求的流程上有很多相似之处,在这里使用 Interceptor 机制可以实现代码结构的优化,还能统一对外提供一种拦截 WebView 内容的机制。一般使用 H5 和原生混合开发的项目都会自定义 WebView 处理通用业务逻辑,加上组件化之类的项目结构影响,添加拦截的位置未必跟 WebView 在同一个 Module。

下面就开始实现吧,代码结构:

把 WebView 的封装放在单独的 Module 里,可以更好地展现解耦的效果。Interceptor 可以跟 WebView 一起预设,也支持在业务层自定义新的。

最终的调用代码就只需要按需求顺序添加 Interceptor 了。

添加的三个 Interceptor 功能分别是监测拦截链耗时,对特定 url 拦截使用缓存和给 url 所有链接添加参数。最后加载的 url 是托管在 gitee 上的一个简单页面,请不要在意内容。计时使用的是 ns,因为测试时几乎所有页面都在 2 ms 内就会加载完。

每个 Interceptor 刚进入和返回前都加了 Log,可以看到 Interceptor 的调用顺序。一个不在缓存范围内的 url 大概是这样的:

由于 AddParamsInterceptor 是最后一个被添加的,它修改的 request 没有被输出到 logcat。

然后可以尝试更多的使用方法了,直接看代码吧~

修改 Request
  • 添加请求参数(GET)

  • 自定义 DNS 解析

修改 Response
  • 自定义本地资源缓存

当然,可能的用法还有更多,具体业务具体分析即可。

知易行难,弄清楚原理和亲自实现一遍流程的难度差距还是很大的。我在实践过程中遇到了一些问题,在这里记录一下,仅供参考。

1. 思路未理清,proceed 方法没接收参数。

chain.proceed 是实现完整调用链的关键,多数 Interceptor 包含的输入和输出由它负责转换。为了保证每个 Interceptor 都有修改输入和修改输出的功能,仅由 Chain 对象持有 url 是不够的。

所以编写这种相对复杂的逻辑的接口时,需要细心确认每个接口的功能,从整体流程到具体实现都依赖接口的定义。先确认一些绝对要遵守的点,比如 Interceptor 必须有 intercept 方法,这个方法必须接受一个 Chain 对象之类的。整体的框架搭起来,后面的思路就不容易偏了。

思考:解耦和分层

解耦是一个软件开发中经常被提到的内容,其重要性想必不用多说。如果把编程看作数据的流动,解耦就是在发生数据传输的两点之间通过规范或契约(接口)的方式代替具体实现,从而分离这相关的两点。

面向对象的编程中需要使用「抽象」的概念,比如对应真实事物的抽象,对应虚拟数据的抽象等。解耦是一种高级的「抽象」,可以将「流程」抽象化。

解耦也不是必须时刻遵守的准则,抽象一个流程需要很多额外的开发量,从平衡代码的可维护性和开发效率以及避免过度设计的角度出发,解耦应该是随业务增长而逐步进行的。

后记

为了提高编程水平,今年开始做了一些尝试,如果有人读到了这里,希望能对本文提点意见建议,或者随意打个分(0~10)。

春节快乐~