2. 应用层拦截器 vs 网络层拦截器:调用时机差异、适用场景、常见陷阱
OkHttp 提供了两种拦截器注册方式:应用层拦截器(Application Interceptor)通过 addInterceptor() 注册,网络层拦截器(Network Interceptor)通过 addNetworkInterceptor() 注册。两者虽然接口签名相同,但在拦截器链中的位置截然不同,直接决定了它们能“看到”的请求/响应内容、能否参与重定向与重试、以及适合哪些场景。误用或混用是后台任务测试中最常见的陷阱之一。
2.1 概念对比
| 对比维度 | 应用层拦截器 | 网络层拦截器 |
|---|---|---|
| 注册方法 | addInterceptor() |
addNetworkInterceptor() |
| 调用时机 | 最外层,在重定向/重试/缓存之前 | 最内层,紧邻实际的网络 I/O |
| 能否看到重定向后的 URL | 不能(只看到原始请求) | 能(每次重定向都会触发) |
| 能否看到重试请求 | 不能 | 能(每次重试都会触发) |
| 能否访问响应体 | 可以,但需手动缓冲 | 可以,流式读取 |
| 是否受缓存策略影响 | 是(缓存命中时拦截器可能不执行) | 否(缓存命中仍会执行) |
| 适用场景 | 统一加 Header、日志打印、请求/响应转换 | 网络监控、重试统计、精确测量网络耗时 |
2.2 调用时机差异(核心流程)
下图展示了 OkHttp 拦截器链的标准执行顺序。应用层拦截器位于最上游,网络层拦截器紧贴真正的网络调用。注意 重定向、重试、缓存 都发生在应用层拦截器与网络层拦截器之间。
2.3 适用场景对比
- 应用层拦截器:
- 统一添加 、、User-Agent 等请求头。
- 打印简洁的请求/响应日志(不包含重定向细节)。
- 整体响应转换(如解压、解密)。
- 后台任务中记录任务级别的网络请求总数(不受重试干扰)。
- 网络层拦截器:
- 精确测量单次网络连接的耗时(包括重试耗时)。
- 监控重试次数、重定向链。
- 实现自定义重试策略(如基于 HTTP 状态码)。
- 跟踪后台任务中每个实际网络请求的成败。
2.4 常见陷阱
陷阱 1:在应用层拦截器中修改请求会被重定向覆盖
如果应用层拦截器添加了某个 Header,但 OkHttp 内部重定向时可能根据 301/302 的 Location 重新构造请求,此时应用层添加的 Header 可能丢失。想确保 Header 每次都携带,应使用网络层拦截器。
陷阱 2:网络层拦截器日志会产生重复输出
由于网络层拦截器每发起一次网络连接就会执行一次(包括重试和重定向),在后台任务测试中,如果按“请求次数”做断言,可能导致计数翻倍。正确的做法是:统计网络请求次数时使用应用层拦截器;分析具体网络性能时使用网络层拦截器。
技巧:如何快速区分当前拦截器属于哪种类型?
在拦截器内打印 chain.request().url 和 chain.request().body。如果访问 body 后报错(IllegalStateException),说明该请求已被消费(常见于网络层拦截器);另外,如果同一请求 URL 出现多次且带有重定向后的路径,说明你处在网络层。
2.5 代码示例:注册两种拦截器
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
// 应用层拦截器:只执行一次,不管重试和重定向
val request = chain.request().newBuilder()
.addHeader("X-App-Id", "my-app")
.build()
val response = chain.proceed(request)
// 记录一次原始请求
Log.d("AppInterceptor", "Status: ${response.code}")
response
}
.addNetworkInterceptor { chain ->
// 网络层拦截器:每次真实网络调用都会执行
val start = System.currentTimeMillis()
val response = chain.proceed(chain.request())
val duration = System.currentTimeMillis() - start
Log.d("NetInterceptor", "URL: ${chain.request().url}, " +
"Duration: ${duration}ms, Code: ${response.code}")
response
}
.build()
调试建议:
在后台任务测试中,建议同时注册两种拦截器,并用不同的日志 Tag(如 AppInterceptor 与 NetInterceptor)加以区分。这样既能获得任务级别的请求概览,又能精确分析网络层的重试行为。
2.6 性能与稳定性注意事项
- 不要在应用层拦截器中缓冲整个响应体,除非必须这样做(例如统一解密),否则会消耗大量内存且破坏流式传输。
- 网络层拦截器中避免执行耗时操作,例如同步写入数据库或等待锁,否则会阻塞 OkHttp 的线程池,影响并发请求。
- 后台任务中谨慎使用
addNetworkInterceptor实现重试,因为它的异常会直接暴露给应用层,需要正确处理IOException类型。