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

前置阅读:建议先读 第 5 篇:CPU 采集入门、第 6 篇:8 个坑 和 第 7 篇:CPU 降频了,你的数据还准吗。
整机 CPU 42%,报告写"正常"。90 分钟后 ANR。 事后查数据:有一个小核跑到 95%、降频到 408MHz、进程就卡在这个核上。 42% 是 8 个核的平均值。平均值把瓶颈藏起来了。


一、从一次 ANR 说起

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

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

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

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

matlab 复制代码
时间     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 使用率

bash 复制代码
adb shell cat /proc/stat

输出(截取前几行):

yaml 复制代码
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:各核当前频率

bash 复制代码
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"

输出:

makefile 复制代码
cpu0: 408000
cpu1: 408000
cpu2: 1200000
cpu3: 408000
cpu4: 2400000
cpu5: 1800000
cpu6: offline
cpu7: offline

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

命令 3:进程跑在哪个核

bash 复制代码
adb shell cat /proc/12345/stat

输出(关键字段):

yaml 复制代码
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):

python 复制代码
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 快照:

ini 复制代码
── 采集时刻: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) 把离散程度压缩成一个数字:

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

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

ini 复制代码
各核 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.04 ≈ 104%

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

对比一个正常场景:

ini 复制代码
各核 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 架构:

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

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

核心类型 微架构 性能系数
小核 Cortex-A55 1.0
大核 Cortex-A76 2.0
超大核 Cortex-X2 3.0

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

ini 复制代码
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 分叉

matlab 复制代码
时间     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%,发现负载不均衡

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

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

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

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

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

Step 4:算实际算力

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

Step 5:汇总分析

erlang 复制代码
┌─────────────────────────────────────────────────┐
│  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 团队(优化后台)

用上面的案例验证:

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

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

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

系列指标层级递进

ini 复制代码
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.5 scaling_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 不高但就是卡"的场景,欢迎评论区交流 👇 关注我,后续更新不迷路。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui