25、内存优化实战:常见内存问题案例,优化前后对比,性能数据解读
讲到这里,我得先跟你交个底。
前面聊了那么多理论——Dalvik 怎么分配内存、ART 怎么回收垃圾、GC 的停顿是怎么回事……说实话,这些知识如果只停留在脑子里,那跟没学差不多。真正让你成长的是什么呢?是踩坑。是线上出问题了,你盯着 logcat 和 profiler 一点一点把问题揪出来。
我个人习惯,每次遇到内存问题,都会把案例记下来。今天我就挑几个典型的,跟你聊聊优化前后的对比,以及数据怎么看。
核心观点:内存优化不是玄学,是数据驱动的工程实践。每一个优化动作,都要有数据支撑。
25.1 案例一:Bitmap 大图加载导致 OOM
这个案例太经典了。我记得刚接手一个图片社交 App 时,线上 OOM 率高达 0.8%。用户反馈说「刷着刷着就闪退了」。我一看堆栈,90% 的 OOM 都发生在 BitmapFactory.decodeStream 附近。
问题分析:
- 服务端返回的图片分辨率是 1920x1080,但列表中的 ImageView 只有 200x150
- 直接加载原图,一张图占内存:1920 * 1080 * 4 = 7.9MB
- 列表一次显示 10 张图,就是 79MB,加上缓存池,轻松破 200MB
优化方案:
// 优化前:直接加载原图
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
imageView.setImageBitmap(bitmap);
// 优化后:采样加载
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
int scaleFactor = Math.min(imageWidth / 200, imageHeight / 150);
options.inJustDecodeBounds = false;
options.inSampleSize = scaleFactor;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
imageView.setImageBitmap(bitmap);
优化前后数据对比:
| 指标 | 优化前 | 优化后 | 改善幅度 |
|---|---|---|---|
| 单图内存占用 | 7.9 MB | 0.3 MB | 96.2% |
| 列表页峰值内存 | 210 MB | 45 MB | 78.6% |
| OOM 率 | 0.8% | 0.02% | 97.5% |
| GC 触发频率 | 每 3 秒一次 | 每 15 秒一次 | 80% |
我的经验:inSampleSize 一定要用 2 的幂次方,否则解码效率会下降。另外,inJustDecodeBounds 这个技巧,我建议你记在脑子里——它只读取图片的宽高信息,不加载像素数据,几乎不占内存。
25.2 案例二:Handler 导致 Activity 泄漏
这个坑我踩过不止一次。说白了,就是内部类持有外部类的引用,导致 Activity 无法被 GC 回收。
问题代码:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 更新 UI
textView.setText("收到消息");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 发送一个延迟消息
mHandler.sendMessageDelayed(Message.obtain(), 60000);
}
}
你想想看,这个 Handler 是匿名内部类,它隐式持有 MainActivity 的引用。如果用户在 60 秒内退出了 Activity,这个延迟消息还在消息队列里,Handler 就不会被回收,Activity 也就泄漏了。
优化方案:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityRef;
MyHandler(MainActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityRef.get();
if (activity != null) {
activity.textView.setText("收到消息");
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 移除所有未处理的消息和回调
mHandler.removeCallbacksAndMessages(null);
}
}
优化前后数据对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 泄漏的 Activity 数量 | 每次操作泄漏 1 个 | 0 |
| 堆内存中 Activity 实例数 | 持续增长,最高 15 个 | 始终 1 个 |
| GC 回收后内存回落 | 无法回落 | 正常回落 |
避坑指南:我曾经在一个项目中,Handler 泄漏导致 Activity 泄漏,进而导致整个 View 树和关联的 Bitmap 都无法回收。一个泄漏点,拖垮了整个应用的内存。所以,我建议你养成两个习惯:一是 Handler 用静态内部类 + WeakReference,二是在 onDestroy 里 removeCallbacksAndMessages。
25.3 案例三:内存抖动(Memory Churn)
内存抖动不像 OOM 那么明显,但它会让你的 App 卡成 PPT。说白了,就是在短时间内频繁创建和销毁对象,导致 GC 被反复触发。
问题场景:
一个列表页,每帧都在创建新的对象。比如在 getView 里 new 了一个 StringBuilder,或者每次绘制都 new 了一个 Paint。
// 问题代码:每帧创建新对象
@Override
public void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每帧都 new
paint.setColor(Color.RED);
canvas.drawCircle(x, y, radius, paint);
}
优化方案:
// 优化后:复用对象
private Paint mPaint = new Paint(); // 只创建一次
@Override
public void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
canvas.drawCircle(x, y, radius, mPaint);
}
优化前后数据对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| GC 触发频率 | 每 1.5 秒一次 | 每 10 秒一次 |
| GC 暂停时间占比 | 12% | 1.5% |
| 帧率(FPS) | 30-45,频繁掉帧 | 55-60,稳定 |
| 内存分配速率 | 8 MB/s | 0.5 MB/s |
我的经验:用 Profiler 看内存分配速率,如果超过 2 MB/s 就要警惕了。我一般会打开 Allocation Tracking,看看是哪个方法在疯狂分配对象。嗯,这个方法很管用。
25.4 内存优化核心流程
说了这么多案例,我想给你总结一个通用的优化流程。这个流程我在多个项目里验证过,效果不错。
25.5 性能数据解读要点
最后,我想聊聊怎么看数据。很多同学拿到 Profiler 截图,不知道重点看什么。我分享一下我的习惯:
- 看趋势,不看绝对值——内存占用 200MB 不一定有问题,但如果持续增长不回落,那就有泄漏嫌疑。
- 看 GC 频率和类型——如果 Concurrent GC 频繁触发,说明内存分配太快;如果 Full GC 出现,说明堆内存紧张。
- 看分配速率——超过 2 MB/s 就要警惕内存抖动。
- 看对象存活数——Activity、Fragment 实例数是否异常增长。
- 对比优化前后——同一个场景,同样的操作路径,对比内存曲线。
一句话总结:内存优化不是一次性工作,而是一个持续迭代的过程。每次发版前跑一遍 Profiler,养成习惯,你的 App 会越来越稳。
公众号:蓝海资料掘金营,微信deep3321