Android CPU 高这个问题,真正难的不是"看到 CPU 高",而是分清它高在哪一层------是 App 用户态算多了、还是内核态 sys_* 在耗、还是被绑在小核降频了、还是后台 SDK 在偷跑。下面把分析链路 + 工具 + 4 个真实案例串一遍。
一、分析链路(从粗到细 4 层)
整机 CPU% (top) → 进程 PID (top -p) → 线程 TID (top -H -p) → 抓 trace 定位函数
第 1 层:整机扫一眼
adb shell top -m 10 -d 1 # 看整机 + 前 10 进程
adb shell top -H -p -d 1 # 看目标进程下各线程 TID
先在 top 里确认是不是你的进程,再看是哪个线程(main? RenderThread? 某个自己起的 worker?)。
第 2 层:perfetto 抓调度 + CPU 频率(推荐,比 systrace 新)
简易 10s 抓取
adb shell perfetto
-c /data/misc/perfetto-traces/config.pbtxt
-o /data/misc/perfetto-traces/trace.pftrace
-t 10s
config 里至少要开:
• sched/sched_switch、sched/sched_wakeup(线程调度)
• power/cpu_frequency(看有没有降频)
• linux.process_stats(进程级 RSS/CPU)
抓完 adb pull 到本地,丢 https://ui.perfetto.dev 打开。
第 3 层:AS Profiler 双保险
• System Trace:看 VSYNC、SurfaceFlinger、线程状态(Running/Sleep/Uninterruptible)
• Method Trace / Call Stack Sample:直接定位 Java/Kotlin 热点函数
第 4 层:perfetto SQL 定量(进阶)
-- 看进程内各线程 CPU 总耗时 TOP
SELECT thread_name, process_name,
SUM(dur)/1e6 AS total_ms, COUNT(*) AS cnt
FROM sched_slice
WHERE process_name = 'com.example.app' AND dur > 100000
GROUP BY thread_name ORDER BY total_ms DESC LIMIT 10;
比肉眼瞄时间轴准得多。
二、perfetto 里必看的两个区分
💡 很多人卡在这一步:绿色 Running 切片 ≠ 你的代码在忙
• Wall ≈ CPU → 纯计算重,去火焰图找热点函数
• Wall >> CPU → 在等调度 / 锁 / IO,看 thread_state 的 R/S/D 分布,找 sys_futex/sys_read 这类内核切片
• Running 但火焰图空 → 大概率陷在长系统调用里(IO、锁),不是你 Java 代码的问题
还有一个反直觉点:整机 CPU 42%,但你进程单核 89%、依然 ANR------因为 8 核机器里你可能被绑在小核,大核空闲+小核降频,整机均值好看但你的核已经炸了。要看 processor 字段 + cpu_frequency 轨道确认。
三、4 个真实案例
案例 1:ListView 疯狂刷 UI(最常见的"假计算")
现象:点一个按钮 CPU 从 <1% 飙到 4%,测试反馈卡。
定位:AS Profiler System Trace → VSYNC 信号极其密集 → 放大峰顶,obtainView 不停跑 → 换 Method Trace 追到 LogViewer 这个控件。
根因:这个 ListView 是用来在 APP 内显示 logcat 的,每打一行日志就 notifyDataSetChanged() 全量刷新整个 list。UI 线程在死循环式 layout。
修复三选一:
- 粗暴:直接砍掉这个 UI → CPU 1.1%(但需求没了)
- TextView 替代 ListView,append 追加日志 → CPU 1.7%
- 缓存日志、500ms 批量刷一次 → CPU 2.4%
这种属于"看着像计算重,其实是 UI 线程被绑架",Wall ≈ CPU 但热点在 Choreographer#doFrame 里。
案例 2:while + for 嵌套,TextView 字符遍历
现象:多开几个页面就卡死,怀疑 CPU 高。
定位:AS Monitors 录 CPU trace → 直接看到 EaseSmileUtils.addSmiles() 占满 CPU → 点进去是 while 遍历 TextView 所有字符 + 内层 for 匹配表情,页面 TextView 一多,单次调用上万次循环。
根因:表情替换算法 O(n²),主线程同步跑。
修复:预编译正则 + 单遍扫描,或移到子线程 + 结果回主线程设 text。
这种是真·计算重,火焰图里你的函数直接顶天,Wall ≈ CPU,没悬念。
案例 3:单核 89% 但整机 42%,ANR 了
这是个进阶坑:
指标 值 含义
整机 CPU% 42% 看起来还行
目标进程单核 89% 已经炸了
processor 0(小核) 被绑小核
cpu0 频率 408MHz 严重降频
温度 78°C thermal throttling
根因叠加三层:
- App 层:主线程计算量确实大
- 调度层:cpuset 策略把进程从小核迁走后再没回大核
- 热设计层:78°C 触发降频
修复优先级:先查 cpuset/affinity 为什么困小核 → 拆主线程计算 → 压测加散热/降持续负载。
只看 top 整机 % 会被骗,必须看单核 + 频率 + 温度三件套。
案例 4:后台"偷 CPU"四兄弟
不是你 App 前台代码的问题,是后台没释放导致的隐性高占:
- AdView 没调 stopLoading(),退后台还在拉广告
- WebView 没调 onPause(),页还在跑 JS
- SensorManager 没 unregisterListener,传感器不停回调
- 推送/保活线程(KeepAliveThread) + PARTIAL_WAKE_LOCK 没释放,CPU 一直醒着(Facebook/Messenger 当年电池门就是这个路子)
隐性 GC 型:FinalizerDaemon / ReferenceQueueDaemon 长期 RUNNABLE 但调栈空 → 频繁创建临时对象 → GC 后 Finalizer 扫不完 → 反过来又促发 GC,恶性循环。
四、常见原因速查表
类别 典型表现 排查入口
UI 线程重绘风暴 VSYNC 密集、notifyDataSetChanged 滥调 System Trace → VSYNC 轨道
主线程死循环/重算法 火焰图顶天、Wall≈CPU Method Trace / Call Stack Sample
锁竞争 / Binder 风暴 Wall>>CPU、sys_futex 长、Runnable 多 perfetto sched_slice + wakeup 频次
小核困住 + 降频 单核高、整机不高、频率低、温度高 cpu_frequency + processor 字段
后台 SDK 偷跑 切后台 CPU 仍不降 top -H 看保活线程 + wakelock
频繁 GC FinalizerDaemon 占 RUNNABLE AS Profiler Memory + Threads tab
五、推荐的排查顺序(实战口诀)
- top -H -p 先定线程------是哪个 TID 在吃
- perfetto 10s 抓调度+频率------看 Running/Sleep、看绑哪个核、看降频没
- Wall vs CPU 判类型------≈ 就去火焰图,>> 就去查锁/IO/唤醒链
- AS Profiler Method Trace 定位函数------Java 层热点直接出类名方法名
- 后台场景单独验------home 出去 30s 再看 top,没掉下来就是案例 4 那种
如果想再往下挖,native 层可以用 simpleperf 录 CPU 采样(ARM PMU 级别,比 Java 采样准),sys_* 长切片 那种内核态问题基本只能靠 perfetto + simpleperf 联合看。要不要顺着把 simpleperf 这套也铺一下?