深入浅出分析内存与CPU不足导致的ANR问题
一、内存不足如何引发ANR?
1. 内存不足的表现
- 频繁GC(垃圾回收):主线程被频繁打断
- OOM前兆:内存抖动导致界面卡顿
- 低内存杀进程:后台进程被回收后需要冷启动
2. 内存问题在traces中的特征
log
"main" prio=5 tid=1 Runnable
at android.os.MessageQueue.nativePollOnce(Native Method)
at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:233)
"FinalizerDaemon" prio=5 tid=3 Waiting
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:442)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:190)
关键特征:
- 出现大量
FinalizerDaemon
线程活动 - 主线程频繁进入
Native
状态(GC导致)
3. 典型案例分析
场景:图片加载未优化
java
// 错误示例:每次滑动都创建新Bitmap
recyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrolled() {
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/large_image.jpg");
imageView.setImageBitmap(bitmap); // 未复用旧Bitmap
}
});
traces表现:
- 主线程频繁执行
decodeFile
- 伴随
GC_FOR_ALLOC
日志 - 内存曲线呈锯齿状(内存抖动)
二、CPU不足如何引发ANR?
1. CPU不足的常见原因
原因 | 影响 |
---|---|
过热降频 | CPU性能骤降 |
后台高负载进程 | 抢占计算资源 |
死循环/过度计算 | 单核满载 |
2. CPU问题在traces中的特征
log
"main" prio=5 tid=1 Runnable
at com.example.app.MainActivity$1.run(MainActivity.java:67) // 计算密集型代码
at android.os.Handler.handleCallback(Handler.java:751)
"RenderThread" prio=5 tid=4 Waiting
at java.lang.Object.wait(Native Method) // 渲染线程等待
关键特征:
- 主线程长时间处于
Runnable
状态 - 系统关键线程(如RenderThread)处于等待
- CPU使用率100%的核与主线程绑定的核一致
3. 典型案例分析
场景:主线程复杂计算
java
// 错误示例:主线程解析复杂JSON
button.setOnClickListener(v -> {
String json = loadHugeJsonFromAsset(); // 10MB JSON
Data data = new Gson().fromJson(json, Data.class); // 同步解析
});
traces表现:
- 主线程卡在JSON解析方法
- 无锁竞争和IO等待
- 对应时间段CPU监控显示单核满载
三、综合问题分析流程
1. 诊断内存问题
graph TD
A[查看traces] --> B{发现频繁GC?}
B -->|是| C[检查内存分配]
B -->|否| D[排除内存因素]
C --> E[分析MAT报告]
E --> F[定位内存泄漏/抖动]
工具使用:
bash
# 监控内存分配
adb shell dumpsys meminfo <package>
# 生成HPROF文件
adb shell am dumpheap <package> /data/local/tmp/heap.hprof
adb pull /data/local/tmp/heap.hprof .
2. 诊断CPU问题
graph TD
A[查看traces] --> B{主线程Runnable时间长?}
B -->|是| C[检查CPU频率]
B -->|否| D[排除CPU因素]
C --> E[使用systrace]
E --> F[定位计算热点]
工具使用:
bash
# 监控CPU频率
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
# 生成systrace
python systrace.py cpu freq -o trace.html
四、优化方案对比
内存优化方案
问题类型 | 解决方案 | 效果 |
---|---|---|
内存泄漏 | LeakCanary检测 + 弱引用 | 减少OOM |
内存抖动 | 对象池 + 缓存控制 | 平滑GC曲线 |
大内存分配 | Bitmap优化 + 分页加载 | 降低峰值内存 |
CPU优化方案
问题类型 | 解决方案 | 效果 |
---|---|---|
计算密集型 | 切线程 + 算法优化 | 降低主线程负载 |
过热降频 | 任务调度策略调整 | 维持稳定性能 |
线程竞争 | 锁优化 + 并发控制 | 提高多核利用率 |
五、经典复合问题案例
案例:列表滑动卡顿ANR
现象:
- 快速滑动RecyclerView时触发ANR
- traces显示主线程阻塞
- 同时伴随内存警告日志
分析:
-
内存角度:
- 发现每次滑动都加载新图片
- 内存抖动导致频繁GC
-
CPU角度:
- 图片解码占用大量CPU
- 主线程解码导致渲染延迟
解决方案:
java
// 使用Glide优化图片加载
Glide.with(context)
.load(url)
.override(targetWidth, targetHeight)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView);
// 增加滑动暂停加载
recyclerView.addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(int state) {
if (state == SCROLL_STATE_DRAGGING) {
Glide.with(context).pauseRequests();
} else {
Glide.with(context).resumeRequests();
}
}
});
六、预防性监控体系建设
1. 内存监控指标
java
// 获取内存状态
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
((ActivityManager)getSystemService(ACTIVITY_SERVICE)).getMemoryInfo(memInfo);
boolean isLowMemory = memInfo.lowMemory;
// 监控关键值
Debug.getNativeHeapAllocatedSize() // Native内存
Runtime.getRuntime().totalMemory() // Java堆内存
2. CPU监控指标
java
// 获取CPU使用率
/proc/stat
/proc/<pid>/stat
// 监控关键值
Thread.getThreadCpuTime(threadId) // 线程CPU耗时
SystemClock.currentThreadTimeMillis() // 线程执行时间
3. 线上监控架构
graph LR
A[客户端] --> B{资源监控}
B -->|内存超标| C[降级策略]
B -->|CPU过热| D[限流策略]
B -->|正常| E[持续运行]
C --> F[上报日志]
D --> F
F --> G[大数据分析]
七、总结与应对策略
内存问题应对
- 预防:定期内存检测 + LeakCanary
- 监控:线上OOM率 + 内存曲线
- 优化:图片加载 + 数据结构选择
CPU问题应对
- 预防:性能测试 + 发热监控
- 监控:CPU使用率 + 线程耗时
- 优化:算法优化 + 异步拆分
复合问题黄金法则:
先解决内存问题(减少GC),再解决CPU问题(提高计算效率),最后优化锁竞争(减少等待)