2. 应用层拦截器 vs 网络层拦截器:调用时机差异、适用场景、常见陷阱

OkHttp 提供了两种拦截器注册方式:应用层拦截器(Application Interceptor)通过 addInterceptor() 注册,网络层拦截器(Network Interceptor)通过 addNetworkInterceptor() 注册。两者虽然接口签名相同,但在拦截器链中的位置截然不同,直接决定了它们能“看到”的请求/响应内容、能否参与重定向与重试、以及适合哪些场景。误用或混用是后台任务测试中最常见的陷阱之一。

2.1 概念对比

对比维度 应用层拦截器 网络层拦截器
注册方法 addInterceptor() addNetworkInterceptor()
调用时机 最外层,在重定向/重试/缓存之前 最内层,紧邻实际的网络 I/O
能否看到重定向后的 URL 不能(只看到原始请求) 能(每次重定向都会触发)
能否看到重试请求 不能 能(每次重试都会触发)
能否访问响应体 可以,但需手动缓冲 可以,流式读取
是否受缓存策略影响 是(缓存命中时拦截器可能不执行) 否(缓存命中仍会执行)
适用场景 统一加 Header、日志打印、请求/响应转换 网络监控、重试统计、精确测量网络耗时

2.2 调用时机差异(核心流程)

下图展示了 OkHttp 拦截器链的标准执行顺序。应用层拦截器位于最上游,网络层拦截器紧贴真正的网络调用。注意 重定向、重试、缓存 都发生在应用层拦截器与网络层拦截器之间。

应用层拦截器 vs 网络层拦截器 调用链 OkHttp 拦截器链执行顺序 Application Interceptor Redirect / Retry / Cache (由 OkHttp 内部自动完成) Network Interceptor 网络 I/O 网络 I/O(响应) 连接 / 写请求 / 读响应 响应经缓存 / 重定向处理 应用层拦截器收响应 重试/重定向时网络层会多次执行 💡 说明: 请求方向:Application Interceptor → 内部重定向/重试/缓存 → Network Interceptor → 网络 I/O。 响应方向反向流回。重定向/重试只会触发 Network Interceptor 再次执行,Application Interceptor 只执行一次。

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 类型。