1.背景与目标
本文介绍 Android 工程中网络请求性能监控和网络切换监控的实现方式。
目标是:
- 对一次 HTTP/HTTPS 请求从发起到结束的完整链路进行一次汇总上报;
- 分阶段统计 DNS、网络连接、TLS、请求和响应的耗时;
- 在默认网络可用或网络类型发生变化时上报一次网络切换事件;
- 不让日志采集或埋点上报影响原始网络请求;
- 避免记录请求体、响应体、Header 值和 URL query 参数等敏感或高基数内容。
2. 整体架构
text
appInit
└─ android_network_log_open
├─ GlobalApp.contextVar.networkLogOpen
└─ NetworkConnectivityMonitor.update(enabled)
接口请求
└─ ApiManager / DynamicUrlManager
└─ OkHttp EventListener.Factory
└─ NetworkEventListener
├─ LogUtils 输出本地汇总日志
└─ NetworkLogReporter
└─ StatService
└─ SensorsStat.onEvent("networkLog", data)
系统默认网络变化
└─ ConnectivityManager.NetworkCallback
└─ NetworkConnectivityMonitor
└─ NetworkLogReporter
└─ StatService
└─ SensorsStat.onEvent("networkChange", data)
3. 开关和初始化时机
初始化流程:
Application.setUpService()将StatService.instance注入到GlobalApp.contextVar.networkLogReporter;- 首次启动请求
discover/appInit; DynamicWritingManger缓存switchConfigListList;- 读取
android_network_log_open,默认值为false; - 将结果写入
GlobalApp.contextVar.networkLogOpen; - 调用
NetworkConnectivityMonitor.update(networkLogOpen)。
开关行为
false:不采集接口性能回调,不注册网络切换回调;true:注册系统默认网络回调,并对后续 OkHttp 请求开启采集;NetworkEventListener在每次callStart()时读取一次开关;已创建的 OkHttpClient 不需要重建;- 关闭开关时会注销
ConnectivityManager.NetworkCallback并清空监控状态; appInit请求发起时开关通常尚未下发,因此该请求通常不会被这套接口性能日志记录;MainViewModel的缓存回退结果也会进入成功处理流程,因此缓存中的开关配置仍可能生效。
4. 接口监控埋点
4.1 接入位置
OkHttpClient中接入
java
.eventListenerFactory(NetworkEventListener.factory())
4.2 一次请求只上报一次
NetworkEventListener是按 OkHttpCall` 创建的。每个请求实例维护自己的时间戳和采集字段。
请求开始时:
text
callStart()
├─ 读取 networkLogOpen
├─ 记录 callStartNanos
└─ 保存 method 和 URL
请求结束时:
callEnd():以outcome=success汇总;callFailed():以outcome=failed汇总;logged标记保证同一个请求不会重复汇总上报。
4.3 请求阶段和回调含义
典型 HTTPS 请求顺序如下:
java
callStart
↓
dnsStart / dnsEnd
↓
connectStart / connectEnd
↓
secureConnectStart / secureConnectEnd
↓
connectionAcquired
↓
requestHeadersStart / requestHeadersEnd
↓
requestBodyStart / requestBodyEnd
↓
responseHeadersStart / responseHeadersEnd
↓
responseBodyStart / responseBodyEnd
↓
callEnd 或 callFailed
| 阶段 | OkHttp 回调 | 统计内容 |
|---|---|---|
| 请求开始 | callStart |
总耗时起点、请求方法、URL、开关状态 |
| DNS 开始 | dnsStart |
DNS 开始时间、解析域名 |
| DNS 结束 | dnsEnd |
DNS 耗时、DNS 成功状态;当前实现不保存 DNS 返回的全部 IP |
| TCP 连接开始 | connectStart |
TCP 建连开始时间、目标 IP |
| TCP 连接结束 | connectEnd |
建立连接耗时、协商后的 HTTP 协议 |
| TLS 开始 | secureConnectStart |
HTTPS TLS 握手开始时间 |
| TLS 结束 | secureConnectEnd |
TLS 握手耗时 |
| 获取连接 | connectionAcquired |
是否复用连接池连接、补充远端 IP |
| 请求头 | requestHeadersStart / requestHeadersEnd |
发送请求头耗时 |
| 请求体 | requestBodyStart / requestBodyEnd |
发送请求体耗时、请求体字节数 |
| 响应头 | responseHeadersStart / responseHeadersEnd |
接收响应头耗时、HTTP 状态码 |
| 响应体 | responseBodyStart / responseBodyEnd |
接收响应体耗时、响应体字节数 |
| 请求结束 | callEnd |
总耗时、传输层成功 |
| 请求失败 | callFailed |
总耗时、最终 IO 失败原因 |
4.4 接口埋点字段
事件名:networkLog
| 字段 | 含义 |
|---|---|
event_type |
固定为 request |
outcome |
success 或 failed,表示传输层结果 |
method |
HTTP 方法,如 GET、POST |
url |
仅保留 scheme、host、port 和 path,不包含 query |
code |
HTTP 状态码;未收到响应时为 -1 |
protocol |
实际协议,如 http/1.1、h2 |
reused |
是否复用已有连接 |
dns_domain |
执行 DNS 解析的域名;连接复用时为空 |
dns_status |
success、failed、not_started 或 unknown |
dns_error |
DNS 失败异常;正常或未执行时为空 |
connected_ip |
Socket 实际连接 IP,不是 DNS 返回的全部 IP;无法获取时为空 |
network_type |
请求结束时默认网络类型,如 wifi、cellular、vpn |
dns_ms |
DNS 解析耗时,单位毫秒 |
connect_ms |
建立网络连接耗时,不包含 DNS;TLS 单独统计 |
tls_ms |
HTTPS TLS 握手耗时;非 HTTPS 或未发生时为 -1 |
request_headers_ms |
发送请求头耗时 |
request_body_ms |
发送请求体耗时 |
response_headers_ms |
接收响应头耗时 |
response_body_ms |
接收响应体耗时 |
total_ms |
从 callStart 到结束或失败的总耗时 |
request_bytes |
已发送请求体字节数,不含请求头 |
response_bytes |
已接收响应体字节数,不含响应头 |
failure |
最终失败异常;成功时为空 |
没有发生或无法采集的阶段统一使用 -1,便于区分"耗时为 0"和"该阶段未执行"。
4.5 示例:书城接口请求
以下示例表示一次使用 Wi-Fi 的 HTTPS 请求:
java
{
"event_type": "request",
"outcome": "success",
"method": "POST",
"url": "https://api.example.com/book/list",
"code": 200,
"protocol": "h2",
"reused": false,
"dns_domain": "api.example.com",
"dns_status": "success",
"dns_error": "",
"connected_ip": "203.0.113.10",
"network_type": "wifi",
"dns_ms": 12,
"connect_ms": 34,
"tls_ms": 48,
"request_headers_ms": 1,
"request_body_ms": 5,
"response_headers_ms": 86,
"response_body_ms": 23,
"total_ms": 214,
"request_bytes": 128,
"response_bytes": 4096,
"failure": ""
}
说明:connect_ms 按 OkHttp 的 TCP 建连阶段统计,不包含 DNS;HTTPS 的 TLS 握手单独进入 tls_ms。connected_ip 表示最终 Socket 实际连接的 IP,当前实现没有上报 DNS 返回的全部候选 IP。
5. 网络切换埋点
5.1 实现方式
NetworkConnectivityMonitor 使用 Android 系统的:
java
ConnectivityManager.registerDefaultNetworkCallback()
监听当前系统默认网络,而不是监听某个具体接口。
5.2 回调处理规则
| 系统回调 | 当前处理 |
|---|---|
onAvailable |
获取网络能力,识别网络类型并判断是否需要上报 |
onCapabilitiesChanged |
更新网络能力;默认网络对象或网络类型变化时上报 |
onLost |
如果丢失的是当前默认网络,则上报网络不可用;如果已有新默认网络,则按网络可用切换处理 |
当前网络类型包括:
java
vpn、wifi、cellular、ethernet、bluetooth、wifi_aware、lowpan、other、none
5.3 网络切换字段
事件名:networkChange
| 字段 | 含义 |
|---|---|
event_type |
固定为 network_change |
network_status |
available 或 lost |
network_type |
当前目标网络类型 |
from_network_type |
切换前网络类型 |
to_network_type |
切换后网络类型 |
from_network_validated |
切换前是否通过系统网络验证 |
network_validated |
切换后是否通过系统网络验证 |
event_time_ms |
Unix 时间毫秒值 |
event_elapsed_realtime_ms |
Android 单调时钟毫秒值 |
5.4 示例:Wi-Fi 切换到移动网络
java
{
"event_type": "network_change",
"network_status": "available",
"network_type": "cellular",
"from_network_type": "wifi",
"to_network_type": "cellular",
"from_network_validated": true,
"network_validated": true,
"event_time_ms": 1778822400000,
"event_elapsed_realtime_ms": 23891234
}
6. 关键边界行为
6.1 连接复用
如果 OkHttp 复用连接池中的连接,通常不会重新执行 DNS、TCP 建连和 TLS 握手:
reused=true;dns_status=not_started;dns_ms、connect_ms、tls_ms可能为-1;connected_ip会尝试从已有连接补充。
6.2 DNS 解析 IP 的口径
dnsEnd() 回调参数中包含 DNS 返回的 inetAddressList,但当前实现没有保存该列表。埋点中的 connected_ip 来自 connectStart() 的实际连接地址,连接复用时则从已有 Socket 地址补充。因此当前埋点可以回答"实际连到了哪个 IP",不能回答"DNS 一共解析出了哪些 IP"。
6.3 HTTP 错误码
HTTP 404、500 等属于"拿到了 HTTP 响应",因此传输层通常进入 callEnd(),表现为:
text
outcome=success
code=404 或 500
outcome 不是业务接口成功标记,而是网络传输是否完整结束的标记。
6.4 DNS 失败
OkHttp 没有单独的 DNS 失败回调。当前实现通过以下条件判断 DNS 失败:
text
dnsStart 已触发 && dnsEnd 未触发 && callFailed 触发
此时:
text
dns_status=failed
dns_error=异常类型:异常消息
6.5 网络验证状态变化
当前 NetworkConnectivityMonitor 的上报判断主要基于默认网络对象和网络类型:
java
shouldReport = network changed || networkType changed
因此,如果只有 network_validated 从 false 变为 true,网络对象和网络类型都不变,当前实现不会单独上报一次切换事件。
6.6 采集失败不影响业务
接口日志输出、传感器上报和网络切换上报均有异常保护。即使日志字段构造或统计 SDK 调用失败,也不会阻断原始网络请求或改变业务流程。