Android CPU 整机 42% 却 ANR?单核分析揭开均值背后的真相

0 阅读12分钟

前置阅读:建议先读 [第 5 篇:CPU 采集入门]、[第 6 篇:8 个坑] 和 [第 7 篇:CPU 降频了,你的数据还准吗]。

整机 CPU 42%,报告写"正常"。90 分钟后 ANR。 事后查数据:有一个小核跑到 95%、降频到 408MHz、进程就卡在这个核上。 42% 是 8 个核的平均值。平均值把瓶颈藏起来了。


一、从一次 ANR 说起

24 小时稳定性压测,整机 CPU% 曲线平稳在 35~45%。第 60 分钟用户反馈卡顿,第 90 分钟 ANR。

只看整机数据,完全看不出问题:

时间     整机 Raw%   整机 Norm%   看起来
0min     32%        30%          正常
30min    38%        32%          正常
60min    42%        22%          ← Normalized 开始掉了
90min    40%        15%          ← ANR 发生

拿到各核数据后,真相浮出水面:

时间     cpu0(小核)  cpu4(大核)  进程所在核   cpu0频率
0min     35%        28%         cpu4        1800MHz
30min    52%        22%         cpu4        1800MHz
60min    90%        12%         cpu0 ←      1200MHz ←
90min    95%        8%          cpu0        408MHz  ←

三个关键变化:

  • 第 60 分钟:进程从大核 cpu4 迁移到了小核 cpu0
  • cpu0 使用率从 35% 飙到 95%:小核算力弱,同样的任务占满了整个核
  • cpu0 频率从 1800MHz 降到 408MHz:发热后被热降频,雪上加霜

整机 42% 是被其他 7 个空闲核"平均"出来的。真正的瓶颈在一个降频的小核上,整机均值完全看不到。


二、实操:三条命令看清各核状态

命令 1:各核 CPU 使用率

adb shell cat /proc/stat

输出(截取前几行):

cpu  128891 3842 52341 882234 12453 0 5819 0 0 0    汇总行
cpu0 18234  512  8123  98234  2341  0 1203 0 0 0    cpu0 单独
cpu1 15672  483  6234  112345 1892  0 892  0 0 0    cpu1 单独
cpu2 17891  501  7456  105678 1456  0 1045 0 0 0
cpu3 16234  462  5892  118234 2134  0 834  0 0 0
cpu4 21345  612  9234  89012  1823  0 923  0 0 0
cpu5 19876  578  8345  95234  1567  0 712  0 0 0
cpu6 12345  398  4123  145678 892   0 145  0 0 0
cpu7 7294   296  2934  117819 348   0 65   0 0 0

两次采样做差值,就能算出各核 CPU%。/proc/stat 第一行是汇总,后面每行是一个核——同一次读取,零额外开销

命令 2:各核当前频率

adb shell "for i in 0 1 2 3 4 5 6 7; do \
  echo -n \"cpu\$i: \"; \
  cat /sys/devices/system/cpu/cpu\$i/cpufreq/scaling_cur_freq 2>/dev/null || echo 'offline'; \
done"

输出:

cpu0: 408000
cpu1: 408000
cpu2: 1200000
cpu3: 408000
cpu4: 2400000
cpu5: 1800000
cpu6: offline
cpu7: offline

一眼看出:小核 cpu0/1/3 被降频到最低档 408MHz,cpu6/7 已关闭。

命令 3:进程跑在哪个核

adb shell cat /proc/12345/stat

输出(关键字段):

12345 (com.example.app) S 1 12345 12345 0 -1 ... 5200 1300 ... 0 0 0 0 2 0 ...
                                                  ^utime ^stime             ^processor
                                                  field14 field15           field39

第 39 个字段 processor(从 1 开始计数):告诉你进程此刻正在哪个核上运行

) 锚点解析更安全(第 6 篇坑 7):

tail = raw.split(')')[1].strip().split()
utime     = int(tail[11])   # field 14
stime     = int(tail[12])   # field 15
processor = int(tail[36])   # field 39

这个字段是判断"进程是不是被调度到了小核"的核心证据,但很多采集工具都没采。


三、把数据拼在一起:一次完整的快照

指标命名约定(避免混淆)

名称公式含义
整机 Raw CPU%(Δtotal−Δidle−Δiowait)/Δtotal整机时间片占比
整机 Normalized CPU%Raw CPU% × 频率比整机等效算力占比(#7 引入)
各核 Raw CPU%各核行独立差值单核时间片占比
单核算力占比各核CPU% × (各核频率/各核最高频率) × IPC系数单核的等效算力贡献(本篇引入)

"整机 Normalized" 看整体降频程度;"单核算力占比" 看每个核的真实产出。

三条命令的数据拼在一起,就得到一个完整的 CPU 快照:

── 采集时刻:90min(ANR 发生时) ──

整机 Raw CPU%:   42.0%
频率比:          (408+408+1200+408+2400+1800+0+0) / (1800×4+2400×2+3000×2)
               = 6624 / 18000 = 0.368
整机 Norm CPU%:  42.0% × 0.368 = 15.5%
在线核:          6 / 8

各核状态:
  核心    类型   CPU%  频率      计算过程                         单核算力占比
  cpu0    小核   95%   408MHz    95%×(408/1800)×1.0 =             21.5%  ← 满载+降频
  cpu1    小核    8%   408MHz     8%×(408/1800)×1.0 =              1.8%
  cpu2    小核   88%  1200MHz    88%×(1200/1800)×1.0 =            58.7%
  cpu3    小核    5%   408MHz     5%×(408/1800)×1.0 =              1.1%
  cpu4    大核   12%  2400MHz    12%×(2400/2400)×2.0 =            24.0%
  cpu5    大核   15%  1800MHz    15%×(1800/2400)×2.0 =            22.5%
  cpu6    超大   关闭  —          —                                 —
  cpu7    超大   关闭  —          —                                 —

  (公式:单核算力占比 = CPU% × (当前频率/最高频率) × IPC系数)

CV:              104%     → 负载严重不均衡(计算过程见第四节)
最高负载核:      cpu0 (95%)
进程所在核:      cpu0 (小核, 408MHz)

这份数据能回答三个问题:

问题数据结论
CPU 忙不忙?Raw 42%整机看不忙
实际干了多少活?Normalized 15.5%算力只有满血的 15.5%
瓶颈在哪?cpu0: 95% @ 408MHz,进程就在 cpu0小核满载 + 降频

四、CV:一个数字判断均值是否可信

各核 CPU% 列表很长,不直观。变异系数(CV) 把离散程度压缩成一个数字:

CV = 标准差 / 均值
CV 范围含义整机 CPU% 可信吗
< 20%各核负载均匀可信
20~50%有不均衡基本可信,建议同时看单核
> 50%严重不均衡不可信

用上面案例的数据(仅在线核)逐步算一遍:

各核 CPU%:  [95%, 8%, 88%, 5%, 12%, 15%](6 核在线)

Step 1: 均值
  mean = (95 + 8 + 88 + 5 + 12 + 15) / 6 = 223 / 6 = 37.2%

Step 2: 各核偏差²
  (95−37.2)² = 3341    ← cpu0 偏离极大
  ( 8−37.2)² =  852
  (88−37.2)² = 2581
  ( 5−37.2)² = 1036
  (12−37.2)² =  635
  (15−37.2)² =  492

Step 3: 标准差
  方差 = (3341+852+2581+1036+635+492) / 6 = 8937 / 6 = 1489.5
  标准差 = √1489.5 = 38.6%

Step 4: CV
  CV = 标准差 / 均值 = 38.6 / 37.2 = 1.04104%

CV > 50% → 整机 42% 这个数字毫无参考价值,必须下钻到单核。

对比一个正常场景:

各核 CPU%:  [35%, 38%, 32%, 40%, 28%, 33%]

mean = 34.3%
标准差 = √[((35−34.3)²+(38−34.3)²+...+(33−34.3)²)/6] = 3.9%
CV = 3.9 / 34.3 = 0.11 = 11%

CV = 11% < 20% → 整机 34% 有代表性,可以直接用。


五、大小核 IPC:同频不同力

现代 Android 几乎都是 big.LITTLE 架构:

小核 (A55):  cpu0~cpu3, 最高 1800MHz
大核 (A76):  cpu4~cpu5, 最高 2400MHz
超大核 (X2): cpu6~cpu7, 最高 3000MHz

同频下,大核 IPC 是小核的 2~3 倍。 对应 Linux 内核 PELT 的 r_cpu 参数:

核心类型微架构性能系数
小核Cortex-A551.0
大核Cortex-A762.0
超大核Cortex-X23.0

单核归一化算力 = CPU% × (当前频率 / 最高频率) × 性能系数

cpu0(小核):95% × (408/1800) × 1.0 = 21.5%
cpu4(大核):12% × (2400/2400) × 2.0 = 24.0%

cpu4 的 CPU% 只有 12%,但实际算力贡献比 95% 满载的 cpu0 还高。如果进程从 cpu0 迁到 cpu4,同样的计算只需占 cpu4 约 5% 的使用率。

性能系数是近似值,精确值需在目标设备上做 benchmark。第一版只做频率归一化(不乘性能系数)就够用,已经比 Raw CPU% 准确得多。


六、完整案例:CPU 42% 为什么 ANR

把上面的知识串起来,走一遍完整的排查流程。

Step 1:看整机数据,发现 Raw 和 Normalized 分叉

时间     Raw%   频率比                  Norm%(=Raw×频率比)    判断
0min     32%    0.94                   32%×0.94=30.1%       两线接近,正常
60min    42%    0.52                   42%×0.52=21.8%       两线分叉,在降频
90min    40%    0.368                  40%×0.368=14.7%      分叉加大,严重降频

Raw 稳定但 Normalized 一路掉 → 设备在降频。

Step 2:查各核 CPU%,发现负载不均衡

90min 各核 CPU%: [95%, 8%, 88%, 5%, 12%, 15%, 关, 关]
CV = 104%

CV >> 50% → 整机 42% 不可信。cpu0 = 95%,已满载。

Step 3:查进程所在核,发现被困在小核

cat /proc/12345/stat → processor = 0
cpu0 类型:小核 A55
cpu0 频率:408MHz(最高 1800MHz)

进程跑在 cpu0(小核),这个核还被降频到了最低档。

Step 4:算实际算力

进程所在核的实际算力 = 95% × (408/1800) × 1.0 = 21.5%
如果跑在 cpu4(大核满频):同任务只需约 5% 使用率

Step 5:汇总分析

┌─────────────────────────────────────────────────┐
│  ANR 时刻数据                                     │
│                                                 │
│  整机 Raw:    42%     ← 看起来正常                │
│  整机 Norm:   15.5%   ← 42%×0.368                │
│  CV:          104%    ← 整机值不可信               │
│  进程所在核:  cpu0 (小核, 95%, 408MHz)             │
│  大核状态:    cpu4 12% @ 2400MHz (大量空闲)        │
│                                                 │
│  瓶颈链:                                        │
│  进程困在小核 → 小核满载 → 温度升高                 │
│  → 热降频到 408MHz → 算力暴跌 → ANR               │
└─────────────────────────────────────────────────┘

七、根因在哪一层

单核瓶颈的根因分布在不同层级。看数据特征判断

层级数据特征谁来修
App 层进程 CPU% 持续 > 85%;主线程有明显耗时操作App 开发(拆线程、减计算)
调度层进程 processor 持续指向小核;大核空闲但不迁移系统团队(查 cpuset/affinity)
热设计层频率比持续下降;温度 > 70°C;time_in_state 集中在低档硬件/散热(改散热方案)
系统负载层最高核不是你的进程,而是 SystemServer 等系统进程ROM 团队(优化后台)

用上面的案例验证:

进程 CPU% = 89%                → App 层有关(主线程计算量大)
processor = 0 且 cpu4 空闲      → 调度层有关(小核困住了)
cpu0 频率 408MHz,温度 78°C     → 热设计层有关(降频严重)
cpu0 最高使用者就是目标进程      → 不是系统负载问题

结论:App 层 + 调度层 + 热设计层的叠加问题。 优先级:

  1. 查 cpuset 策略,为什么进程会从大核迁到小核
  2. 优化主线程计算量,减少对单核的依赖
  3. 长时间压测场景加散热或降低持续负载

系列指标层级递进

Layer 1 (#5 入门)                     Layer 2 (#6 避坑)
  Raw CPU%                              排除 iowait
  = (Δtotal−Δidle)/Δtotal               = (Δtotal−Δidle−Δiowait)/Δtotal
  → 知道"忙不忙"                         → 把 Raw 算对
        │                                       │
        ▼                                       ▼
Layer 3 (#7 降频)                     Layer 4 (#8 单核 = 本篇)
  + 频率比                               + 各核 CPU% → CV
  Normalized = Raw × freq_ratio          + processor → 进程在哪个核
  → 知道"实际干了多少活"                  + IPC 系数 → 单核算力占比
        │                               → 知道"瓶颈在哪个核"
        ▼                                       │
Layer 5 (#9 落地)                                ▼
  完整输出 JSON + 采集代码               判断逻辑:
  + 平台展示 + 压测报告                  CV > 50%? → 整机值不可信
  → 从公式到可交付方案                   进程在小核? → 调度异常
                                        频率比 < 0.4? → 严重降频

每一层指标都建立在前一层之上,只有全部叠加起来才能完整诊断 CPU 问题


小结

指标公式数据来源回答什么
各核 CPU%各核行 (Δtotal−Δidle−Δiowait)/Δtotal/proc/stat 各核行哪个核是瓶颈
CV各核CPU%的标准差/均值各核 CPU% 计算整机均值是否可信
processor直接读取/proc/{pid}/stat 第 39 字段进程跑在哪个核
各核频率直接读取scaling_cur_freq哪个核被降频
单核算力占比CPU% × (频率/最高频率) × IPC系数上述数据组合各核真实产出

什么时候必须看单核

信号阈值数据来源说明
CV 过高> 50%各核 CPU% 计算负载不均衡,整机值不可信
频率比过低< 0.5scaling_cur_freq算力已腰斩,需看哪些核被压了
在线核不足< 总核的 75%/proc/stat 行数核被关了,均值被拉低
整机值"正常"但体感卡Raw < 50% 但有卡顿用户反馈 + Raw CPU%最典型的均值陷阱
进程在小核processor 指向 LITTLE 核/proc/{pid}/stat大核空闲但进程被困小核

任何一条命中,整机 CPU% 就不能独立使用——必须下钻到单核。


下篇预告

CPU 采集落地:从公式到平台的完整方案

本篇讲清了"看什么数据、怎么判断"。下篇给出完整的采集代码、一轮采集的具体输出、2 小时压测的数据全貌、平台展示方案和最终的压测报告模板。 系列终篇,数据说话,拿走就能用。


系列目录

  • 第 1 篇:内存泄漏自动检测(上)——采集层设计
  • 第 2 篇:内存泄漏自动检测(中)——检测层设计
  • 第 3 篇:内存泄漏自动检测(下)——响应层设计
  • 第 4 篇:Android 内存采集避坑指南
  • 第 5 篇:Android CPU 使用率采集入门
  • 第 6 篇:CPU 采集的 8 个坑
  • 第 7 篇:CPU 降频了,你的采集数据还准吗?
  • 第 8 篇(本篇):CPU 单核分析:均值是最大的谎言
  • 第 9 篇(下一篇):CPU 采集落地:从公式到平台的完整方案

我是测试工坊,专注 Android 系统级性能工程。 如果你也遇到过"整机 CPU 不高但就是卡"的场景,欢迎评论区交流 👇 关注我,后续更新不迷路。