**RUM实战:用数据说话的Android网络性能优化**
**概述**
在移动互联网时代,网络请求性能已成为影响用户体验的关键因素。据统计,转化率会随着页面加载时间增加大幅下降,而移动应用中最常遇到的用户投诉都与"加载慢"、"卡顿"等网络性能问题相关。然而,移动端网络环境的复杂性远超Web端:
网络环境多样化
- WiFi、4G/5G、3G、2G等多种网络制式共存
- 信号强弱变化、网络切换频繁
- 不同地域、运营商的网络质量差异巨大
设备碎片化严重
- Android设备品牌、型号众多
- 系统版本从Android 5.0到最新版本跨度大
- 设备性能参差不齐,影响网络处理能力
问题排查困难
- 缺乏可见性:传统监控只能看到请求成功/失败和总耗时,无法了解具体耗在哪个环节
- 难以复现:用户反馈"很慢",但开发环境下往往无法复现
- 缺少量化依据:凭感觉优化,无法评估优化效果
- 端到端追踪缺失:客户端日志缺失,与服务端监控割裂,无法形成完整链路
为了解决上述痛点,我们需要将网络请求的"黑盒"变成"透明盒",清晰地看到每个环节的耗时。阿里云RUM Android SDK提供了移动端网络性能监控能力。接下来,我们将详细介绍RUM SDK采集的资源指标数据模型,帮助你理解每个指标的含义和计算方式。
**资源指标数据说明**
要让每个网络请求的各个阶段都清晰可见、可量化,首先需要建立一套标准化的数据模型。阿里云RUM采用Resource事件作为网络请求监控的核心数据模型。
Resource事件是专门针对网络请求设计的标准化事件类型,它基于HTTP协议和W3C Performance Timing API标准制定,确保了数据采集的准确性和可对比性。由于Performance API在不同平台环境(Web、iOS、Android、HarmonyOS)下存在实现差异,RUM针对这些差异进行了修正和对齐,使得无论是Web端还是移动端,开发者都能看到口径一致的性能数据,方便跨平台性能对比和问题排查。
接下来,我们将详细介绍Resource事件包含的属性字段和指标字段。
**2.1 属性字段说明**
Resource事件包含丰富的属性字段,用于描述请求的上下文信息:
| 属性 | 类型 | 描述 |
|---|---|---|
| session.id | string | 关联的session |
| view.id | string | 关联的view |
| view.name | string | 关联的view.name |
| resource.type | string | 采集的资源类型 (eg: css, javascript, media, XHR, image,navigation). |
| resource.method | string | HTTP 请求方法 (eg: POST, GET). |
| resource.status\_code | string | 资源状态码 |
| resource.message | string | 一般错误时补充的返回结果内容 |
| resource.url | string | 资源 URL |
| resource.name | string | 默认为url的path部分,可以基于规则进行匹配或用户主动配置 |
| resource.provider\_type | string | 资源提供者类型 (eg: first-party, cdn, ad, analytics). |
| resource.trace\_id | string | 资源请求 traceId |
| resource.snapshots | string | 资源的快照JSON String |
**2.2 指标字段说明**
除了属性字段,Resource事件还包含了核心的性能指标,这部分数据是我们排查网络请求慢的核心数据。
| 指标 | 类型 | 描述 |
|---|---|---|
| resource.success | number | 资源加载是否成功,1 表示成功,0表示失败,-1 表示未知 |
| resource.duration | long (ms) | 加载资源所花费的全部时间(responseEnd - redirectStart) |
| resource.size | long (bytes) | 资源大小,对应decodedBodySize |
| resource.connect\_duration | long (ms) | 与服务器建立连接所花费的时间(connectEnd - connectStart) |
| resource.ssl\_duration | long (ms) | TLS 握手所花费的时间。如果最后一个请求不是通过 HTTPS,则不会出现此指标 (connectEnd - secureConnectionStart)。这里需要特别判断一下,如果secureConnectionStart的值为0, 说明没有发起SSL连接,此时不计算ssl\_duration,ssl\_duration赋值为0 |
| resource.dns\_duration | long (ms) | 解析最后一个请求的 DNS 名称所花费的时间 (domainLookupEnd - domainLookupStart) |
| resource.redirect\_duration | long (ms) | 重定向 HTTP 请求上的时间(redirectEnd - redirectStart) |
| resource.first\_byte\_duration | long (ms) | 等待接收响应的第一个字节所花费的时间(responseStart - requestStart) |
| resource.download\_duration | long (ms) | 下载响应所用的时间 (responseEnd - responseStart) |
**2.3 请求耗时阶段说明**
一个完整的HTTPS请求通常包含以下关键阶段:

**2.4 计算口径**
了解了指标的定义,接下来我们深入了解Android端基于OkHttp3的具体计算实现。
**2.4.1 OkHttp3计算口径**
下表展示了Android 网络资源请求各阶段耗时计算口径,明确定义各个阶段的起止时间点和计算方法。
详细时间起始节点可在原始数据的resource.timing_data字段查看。
| 字段名 | 计算公式(OkHttp回调阶段) | 含义(控制台展示) | 说明 |
|---|---|---|---|
| resource.redirect\_duration | callStart - (first)callStart | 重定向耗时 | HTTP重定向的总耗时,从第一次请求到最后一次重定向完成的时间 * 如果没有重定向,该值为 0 |
| resource.dns\_duration | dnsEnd - dnsStart | DNS查询耗时 | 域名解析耗时,将域名解析为IP地址所需的时间 * 如果使用连接池复用连接,该值为 0(因为无需DNS解析) |
| resource.connect\_duration | connectEnd - connectStart | TCP连接耗时 | 与服务器建立连接的总耗时,包含TCP三次握手和SSL/TLS握手时间 * 如果使用连接池复用连接,该值为 0 |
| resource.ssl\_duration | secureConnectEnd - secureConnectStart | SSL安全连接耗时 | SSL/TLS安全连接握手耗时 仅在HTTPS请求时有值,HTTP请求该值为 0 如果使用连接池复用连接,该值为 0 |
| resource.first\_byte\_duration | responseHeadersStart - requestHeadersStart | 请求响应耗时 | 从请求开始发送到收到响应首字节的时间 |
| resource.download\_duration | responseBodyEnd - responseHeadersStart | 内容传输耗时 | 响应体下载耗时,从开始接收响应到完全接收的时间 |
| resource.duration | responseBodyEnd - callStart | 资源加载总耗时 | 资源加载的全部耗时,从请求开始到响应完全接收的总时长 |
说明:控制台展示的"TCP连接耗时"实际上包含了SSL握手时间
2.4.2 连接复用识别
根据RUM SDK采集到的指标数据,我们可以识别连接是否复用,判断依据如下:
判断依据:
- connectionAcquiredTime > 0:连接已获取
- dnsStartTime <= 0:没有DNS解析回调
- tcpStartTime <= 0:没有TCP连接回调
连接复用时的特征:
- resource.dns_duration = 0
- resource.connect_duration = 0
- resource.ssl_duration = 0
- 存在 callStart → connectionAcquired 的等待时间(连接池查找时间)
这个等待时间是一个重要的性能指标,如果过长可能说明连接池配置不当。
2.4.3 TCP与SSL连接关系
对于HTTPS请求,连接建立分为两个阶段:
connectStart (TCP开始)
↓
[TCP三次握手]
↓
secureConnectStart (SSL握手开始)
↓
[SSL/TLS握手]
↓
secureConnectEnd (SSL握手结束)
↓
connectEnd (连接建立完成)
时间关系:
总连接时间 = connectEnd - connectStart
纯TCP时间 = secureConnectStart - connectStart (近似)
SSL时间 = secureConnectEnd - secureConnectStart
**2.5 控制台指标查看**
进入RUM控制台-选择您的应用-点击“API请求”模块-点击具体的一条明细详情,可以查看请求各阶段耗时与耗时分布

理解了数据模型和计算口径后,让我们通过一个真实的线上案例,看看如何利用这些指标数据快速定位性能问题。
**真实用户案例分析**
**3.1 案例背景**
某APP收到线上用户投诉,反馈"页面加载特别慢"、"经常转圈超过1秒"。开发团队第一时间排查后端服务,却发现了一个令人困惑的现象:客户端反馈某核心接口响应时间经常超过1秒(部分用户甚至达到2-3秒),无论WiFi还是4G网络环境都存在此问题,且具有随机性,在开发环境中难以稳定复现;而后端监控却显示接口服务端处理时间稳定在400ms左右,数据库查询性能正常无慢查询,服务器CPU、内存负载也都健康。两边数据对不上!客户端说1.2秒,服务端只用了400ms,那剩下的800ms去哪了?在没有细粒度监控的情况下,团队陷入了"盲人摸象"的困境:客户端和服务端相互甩锅,问题迟迟无法解决。通过接入阿里云RUM Android SDK,我们采集到了详细耗时数据,看看问题是如何被精准定位的。
**3.2 Timing Data原始数据**
在resource.timing_data字段中,我们获取到了请求各阶段的原始时间点(单位:纳秒):
{
"requestHeadersEnd": 1560814315115219,
"responseBodyStart": 1560814719308917,
"requestType": "OkHttp3",
"connectionAcquired": 1560814312934751,
"connectionReleased": 1560814721700948,
"requestBodyEnd": 1560814315850323,
"responseHeadersEnd": 1560814718722250,
"requestHeadersStart": 1560814312975011,
"responseBodyEnd": 1560814719441625,
"requestBodyStart": 1560814315146573,
"callEnd": 1560814721840948,
"duration": 1232825780,
"callStart": 1560813486615845,
"responseHeadersStart": 1560814718314125
}
关键观察:
- 没有 DNS/TCP/SSL 相关的回调时间点→ 说明使用了连接池复用
- callStart 到 connectionAcquired 间隔 826ms → 连接池等待时间异常长
- 总耗时 duration = 1232.8ms
这里已经有了明确的线索:问题不是出在DNS、TCP或SSL握手上,而是等待连接池分配连接的时间过长。
**3.3 详细阶段分析**
基于原始数据,结合2.4的数据计算口径,我们进行逐阶段耗时计算,精准定位性能瓶颈:
**阶段1:等待连接池分配**
callStart → connectionAcquired
耗时: (1560814312934751 - 1560813486615845) / 1,000,000 = 826.32 ms ⚠️
说明:
- 从连接池获取可用连接的等待时间
- 没有 DNS/TCP 回调 = 复用现有连接
- 这是最大的性能瓶颈!占总耗时的67%
**阶段2:发送请求头**
requestHeadersStart → requestHeadersEnd
耗时: (1560814315115219 - 1560814312975011) / 1,000,000 = 2.14 ms ✅
**阶段3:发送请求体**
requestBodyStart → requestBodyEnd
耗时: (1560814315850323 - 1560814315146573) / 1,000,000 = 0.70 ms ✅
**阶段4:等待服务器响应(TTFB)**
requestBodyEnd → responseHeadersStart
耗时: (1560814718314125 - 1560814315850323) / 1,000,000 = 402.46 ms
说明: 服务器处理请求的时间,与后端日志吻合,正常范围内。
**阶段5:接收响应头**
responseHeadersStart → responseHeadersEnd
耗时: (1560814718722250 - 1560814718314125) / 1,000,000 = 0.41 ms ✅
**阶段6:接收响应体**
responseBodyStart → responseBodyEnd
耗时: (1560814719441625 - 1560814719308917) / 1,000,000 = 0.13 ms ✅
**阶段7:连接释放**
responseBodyEnd → connectionReleased
耗时: (1560814721700948 - 1560814719441625) / 1,000,000 = 2.26 ms ✅
通过这个分析,我们清晰地看到连接池等待时间是性能瓶颈。
**3.4 问题诊断**
**异常点诊断**
核心问题:连接池等待时间过长(826ms)
可能的原因:
- 连接池已满 - 所有连接都在使用中,需要等待其他请求释放连接
- 串行请求排队 - 对同一 Host 的请求过多,受限于 maxRequestsPerHost 配置
- 连接泄漏 - 之前的请求没有正确释放连接
- 连接池配置不当 - maxIdleConnections 设置过小
**诊断步骤**
Step 1: 检查连接池配置
// 查看当前OkHttpClient的连接池配置
ConnectionPool connectionPool = okHttpClient.connectionPool();
// 默认配置:最多5个空闲连接,保活5分钟
检查后发现:应用使用的是OkHttp默认配置,只有5个空闲连接。
Step 2: 监控同时进行的请求数量
通过RUM控制台查看该时间段内对同一Host的并发请求数量。
Step 3: 检查是否有连接泄漏
查看应用日志,确认所有请求都正确关闭了Response Body:
Response response = client.newCall(request).execute();
try {
String body = response.body().string();
// 处理响应
} finally {
response.close(); // 必须关闭!
}
诊断结论:
问题是由连接池配置过小导致的。大量请求在等待连接释放,造成严重的性能瓶颈。
明确了问题原因后,接下来我们将介绍常见的网络性能问题排查方法和优化思路。
**常见问题最佳排查实践**
通过上述案例,我们看到了如何利用RUM数据定位问题。本章节将系统性地介绍4类最常见的网络性能问题及其排查方法。
**4.1 连接池等待时间过长**
**症状:**在resource.timing_data中观察到连接获取耗时异常。
callStart → connectionAcquired 耗时 > 500ms
诊断步骤:
**Step 1: 查看连接池配置**
// 检查当前配置
ConnectionPool pool = okHttpClient.connectionPool();
// 默认:5个空闲连接
**Step 2: 查看并发请求数**
通过RUM控制台查看该时间段的并发请求数:
-- 在RUM控制台执行查询
SELECT
COUNT(*) as concurrent_requests
FROM rum_resource
WHERE
timestamp BETWEEN start_time AND end_time
AND resource.url LIKE 'https://api.example.com%'
GROUP BY timestamp
ORDER BY concurrent_requests DESC
**Step 3: 检查连接泄漏**
// 添加日志监控连接池状态
interceptor.addInterceptor(chain -> {
ConnectionPool pool = chain.connection().connectionPool();
Log.d("Pool", "Active: " + pool.connectionCount() +
", Idle: " + pool.idleConnectionCount());
return chain.proceed(chain.request());
});
**优化思路:**
// 方案1:增加连接池大小
.connectionPool(new ConnectionPool(30, 5, TimeUnit.MINUTES))
// 方案2:增加每个Host的最大并发数
.dispatcher(new Dispatcher() {{
setMaxRequestsPerHost(10); // 默认5
setMaxRequests(64); // 默认64
}})
// 方案3:请求合并
**4.2 DNS解析缓慢**
**症状:**在控制台观察到DNS解析耗时持续偏高。
resource.dns_duration > 500ms
诊断步骤:
**Step 1: 确认是DNS问题**
检查 resource.dns_duration 是否持续偏高
检查不同网络环境(WiFi vs 4G)的差异
**Step 2: 分析特定域名**
// 在RUM控制台按域名分组
SELECT
resource.url_host,
AVG(resource.dns_duration) as avg_dns_time,
MAX(resource.dns_duration) as max_dns_time
FROM rum_resource
WHERE resource.dns_duration > 0
GROUP BY resource.url_host
ORDER BY avg_dns_time DESC
**解决思路:**
// 方案1:使用自定义DNS
.dns(new CustomDns())
// 方案2:使用HttpDNS
.dns(new AliHttpDns())
// 方案3:DNS预解析
DnsPreloader.preload(client);
**4.3 SSL握手耗时高**
**症状:**在控制台观察到SSL握手耗时异常。
resource.ssl_duration > 1000ms
诊断步骤:
**Step 1: 确认SSL版本**
// 添加拦截器查看SSL信息
interceptor.addInterceptor(chain -> {
Connection connection = chain.connection();
if (connection != null) {
Handshake handshake = connection.handshake();
if (handshake != null) {
Log.d("SSL", "Protocol: " + handshake.tlsVersion());
Log.d("SSL", "Cipher: " + handshake.cipherSuite());
}
}
return chain.proceed(chain.request());
});
**Step 2: 检查连接复用率**
// 在RUM控制台查询
SELECT
COUNT(CASE WHEN resource.ssl_duration = 0 THEN 1 END) * 100.0 / COUNT(*) as reuse_rate
FROM rum_resource
WHERE resource.url LIKE 'https://%'
**优化思路:**
// 方案1:启用SSL Session复用
.sslSocketFactory(SslConfig.createSSLSocketFactory())
// 方案2:增加连接保活时间
.connectionPool(new ConnectionPool(30, 10, TimeUnit.MINUTES)) // 延长到10分钟
// 方案3:使用证书固定
.certificatePinner(certificatePinner)
**4.4 TTFB过长**
**症状:**从请求发送到收到首字节的时间过长,在控制台上观察“请求响应耗时”较长。
resource.first_byte_duration > 2000ms
诊断步骤:
**Step 1: 排除客户端问题**
确认以下指标正常:
- DNS解析时间 < 300ms
- 连接建立时间 < 500ms
- 请求发送时间 < 100ms
**Step 2: 分析服务器响应时间**
TTFB主要由服务器处理时间决定,如果客户端指标正常,需要:
- 检查服务器负载
- 检查数据库查询性能
- 检查接口业务逻辑复杂度
- 使用APM工具追踪服务端性能
**Step 3: 网络路径分析**
// 通过RUM控制台查看不同地域/运营商的TTFB差异
SELECT
user.region,
user.isp,
AVG(resource.first_byte_duration) as avg_ttfb
FROM rum_resource
GROUP BY user.region, user.isp
ORDER BY avg_ttfb DESC
**优化思路:**
// 方案1:使用CDN加速
// 将静态资源和API部署到CDN节点
// 方案2:启用服务器缓存
// 在服务端实现合理的缓存策略
// 方案3:数据预取
// 在用户可能访问前提前请求数据
PreloadManager.preload("https://api.example.com/user/profile");
// 方案4:请求优先级管理
.dispatcher(new Dispatcher() {{
// 高优先级请求使用单独的线程池
}})
**案例小结**
通过前面4类常见问题的排查方法,我们掌握了系统化的诊断思路。现在,让我们回到第3章那个困扰团队多日的真实案例——连接池等待时间826ms的性能瓶颈。通过RUM数据的精准定位,我们发现问题的根源在于连接池配置不当导致请求排队等待,而解决方案其实很简单:根据不同应用类型选择合适的连接池配置。
配置建议:
针对OkHttpClient的maxIdleConnections参数(默认值为5),建议根据应用特点进行调整,根据经验常见的配置如下:
- 高并发应用:maxIdleConnections = 30-50
这类应用用户活跃度高,网络请求频繁且并发量大,需要充足的连接池支撑 - 一般应用:maxIdleConnections = 10-20
适中的请求频率和并发量,保持适度的连接池规模即可 - 低频应用:maxIdleConnections = 5-10
用户请求较少,保持默认配置或略微增加即可满足需求
从事后优化到主动监控:
然而,这个案例也给我们带来了更深层的思考——性能优化不应该是"亡羊补牢"式的事后补救。除了掌握事后排查和优化的方法,更重要的是建立一套完善的性能监控体系,通过RUM控制台实时掌握应用的网络性能指标,将"被动救火"转变为"主动观测"。如果有需要,还可以基于RUM平台自定义配置告警规则(如连接池等待时间P95 > 500ms时触发通知),进一步提升问题响应速度。
**监控告警配置建议**
RUM数据允许用户创建自定义告警进行实时监控,建立科学的监控告警体系,可以在问题影响用户之前及时发现并处理。
指标告警阈值参考
根据RAIL 模型、 Google Web Vitals 等业界实践,常见的阈值参考如下
| 指标 | 告警阈值 | 严重程度 | 说明 |
|---|---|---|---|
| resource.duration | P95 > 3s | 严重 | 资源加载总耗时 |
| resource.first\_byte\_duration | P95 > 800ms | 警告 | TTFB过长 |
| resource.dns\_duration | P95 > 200ms | 提示 | DNS解析慢 |
| resource.connect\_duration | P95 > 400ms | 警告 | 连接建立慢 |
| resource.ssl\_duration | P95 > 400ms | 提示 | SSL握手慢 |
| 连接池等待时间 | P95 > 500ms | 严重 | 连接池配置不足 |
| 连接复用率 | < 70% | 警告 | 连接未有效复用 |
**总结**
在移动应用开发中,网络请求性能直接影响用户体验。通过接入阿里云RUM Android SDK,开发者可以获得以下核心能力:
精准定位性能瓶颈
- 细粒度的阶段耗时(DNS、TCP、SSL、TTFB等)帮助快速识别问题
- 从"请求慢"的模糊描述,到"连接池等待826ms"的精准定位
连接复用分析
- 自动识别连接池使用效率
- 发现连接泄漏、连接池配置不当等隐藏问题
真实用户体验监控
- 基于真实用户的网络环境采集数据
- 按地域、运营商、网络类型等维度分析性能差异
数据驱动优化
- 优化前后对比清晰可见
- 建立性能基准和告警机制,持续改进
阿里云RUM针对 Android 端实现了对应用性能、稳定性、和用户行为的无侵入式监控采集SDK,可以参考接入文档体验使用。除了Android外,RUM也支持Web、小程序、iOS、鸿蒙等多种平台监控分析,相关问题可以加入“RUM 用户体验监控支持群”(钉钉群号: 67370002064)进行咨询。