1. 拦截器基础概念
在 OkHttp 与 Retrofit 构成的网络层中,拦截器(Interceptor)是插入 HTTP 请求/响应链条的钩子,允许开发者在发送请求前和收到响应后执行自定义逻辑。埋点状态嵌入正是依赖拦截器在关键位置注入采集代码,因此理解拦截器的分类与执行顺序是后续章节的前提。
1.1 OkHttp 拦截器链原理
OkHttp 内部使用责任链模式管理多个拦截器,每个拦截器都有机会处理请求和响应,并决定是否继续向下传递。整个链条分为两大阶段:
- Application 拦截器:通过
addInterceptor()添加,位于链条最外层,对每次请求/响应只触发一次(不论有无网络缓存)。 - Network 拦截器:通过
addNetworkInterceptor()添加,位于链条内层,只有在真正发起网络请求时才会被调用,并会经历重定向和重试。
下图展示了标准 OkHttp 拦截器链的结构与执行顺序:
1.2 Application 与 Network 拦截器区别
两种拦截器在触发时机、可访问数据和典型用途上有显著差异,混淆使用会导致埋点数据缺失或重复。
| 对比维度 | Application 拦截器 | Network 拦截器 |
|---|---|---|
| 触发时机 | 每次请求/响应只执行一次(即使有缓存),未发起网络请求时也会触发 | 仅在实际发起网络请求时触发,可能因重定向/重试执行多次 |
| 可访问数据 | 可以访问 Request、Response、Chain,无法获取连接层原始数据 |
可以访问 Request、Response 以及 Connection,能获取原始字节流 |
| 是否关心缓存 | ❌ 不关心 — 即使命中缓存也会调用 | ✅ 只关心 — 只有真正走网络时才调用 |
| 典型用途 | 统一埋点、鉴权 Header 注入、请求体加密、全局日志 | 网络层日志(含重定向)、重试机制、流量监控、证书校验 |
| 添加方式 | OkHttpClient.Builder.addInterceptor() |
OkHttpClient.Builder.addNetworkInterceptor() |
1.3 执行顺序细节与埋点影响
对于埋点场景,执行顺序决定了埋点数据采集的位置:
- 如果需要在请求发出前记录时间戳、追加公共参数,应使用 Application 拦截器(在最外层,最先执行)。
- 如果需要记录真实网络往返耗时(排除缓存命中场景),应使用 Network 拦截器(仅网络请求时触发,且能捕获重试耗时)。
- 不要混合使用两种拦截器做同一埋点,否则会导致重复计数。
💡 实践提示:官方建议优先使用 Application 拦截器做业务埋点,因为它更稳定,不受重定向影响,且代码可读性更高。
1.4 代码示例:添加两种拦截器
以下代码创建 OkHttpClient 并同时注册两种拦截器:
OkHttpClient client = new OkHttpClient.Builder()
// Application 拦截器(外层)
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.addHeader("X-Trace-Id", UUID.randomUUID().toString())
.build();
long start = System.currentTimeMillis();
Response response = chain.proceed(request);
long duration = System.currentTimeMillis() - start;
Log.d("AppInterceptor", "耗时: " + duration + "ms, URL: " + request.url());
return response;
}
})
// Network 拦截器(内层)
.addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Log.d("NetInterceptor", "网络层触发: " + chain.request().url());
Response response = chain.proceed(chain.request());
Log.d("NetInterceptor", "网络层返回: " + response.code());
return response;
}
})
.build();
1.5 调试建议
开发期间推荐使用 HttpLoggingInterceptor 快速验证拦截器执行顺序:
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging) // Application 层日志
.addNetworkInterceptor(logging) // Network 层日志
.build();
在 logcat 中观察输出,你会看到两次日志(如果成功发起网络请求),分别对应两个层级。
1.6 常见错误
- 阻塞主线程:拦截器中的
chain.proceed()是同步操作,如果放在 UI 线程执行会导致 ANR。务必保证 OkHttpClient 在后台线程发起请求(Retrofit 默认异步执行,无需额外处理)。 - 修改请求体导致数据不一致:在 Application 拦截器中读取
RequestBody会消耗流,再次写入时需要使用Buffer重新构造,否则请求体为空。 - Network 拦截器中修改请求头但未处理重定向:重定向时 Network 拦截器会再次触发,若未区分场景会导致重复添加 Header。
- 混淆两种拦截器的调用次数:如果同一个埋点同时放在 Application 和 Network 拦截器,缓存命中时只执行 Application,网络请求时两者都执行,计数逻辑易出错。
1.7 性能与稳定性优化
| 优化方向 | 具体措施 |
|---|---|
| 避免对象创建 | 拦截器是单例,内部不要频繁创建 Request.Builder 或日志字符串,建议复用或延迟初始化。 |
| 异步日志 | 生产环境避免在拦截器中直接写磁盘或网络,使用消息队列缓存或异步写入。 |
| 限制重试次数 | Network 拦截器中实现重试时需控制最大次数(如 3 次),防止无限制重试消耗带宽。 |
| 连接池复用 | 拦截器不会影响连接池,但不要在拦截器中手工关闭 Response.body(),否则连接无法回收。 |
⚠️ 特别注意:Network 拦截器中调用
chain.proceed() 后会返回一个 Response,该 Response 的 body 只能被消费一次。如果需要在多个拦截器之间共享响应数据,必须使用 PeekSource 或克隆 body。