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 内存优化核心流程

说了这么多案例,我想给你总结一个通用的优化流程。这个流程我在多个项目里验证过,效果不错。

内存优化核心流程 1. 数据采集 2. 问题定位 3. 方案制定 4. 实施优化 5. 验证与监控 关键工具 • Android Profiler • Memory Analyzer (MAT) • LeakCanary • Allocation Tracker • dumpsys meminfo • Systrace • GC 日志分析 • 自定义内存监控 • 线上 OOM 堆栈 • 性能监控平台

25.5 性能数据解读要点

最后,我想聊聊怎么看数据。很多同学拿到 Profiler 截图,不知道重点看什么。我分享一下我的习惯:

  1. 看趋势,不看绝对值——内存占用 200MB 不一定有问题,但如果持续增长不回落,那就有泄漏嫌疑。
  2. 看 GC 频率和类型——如果 Concurrent GC 频繁触发,说明内存分配太快;如果 Full GC 出现,说明堆内存紧张。
  3. 看分配速率——超过 2 MB/s 就要警惕内存抖动。
  4. 看对象存活数——Activity、Fragment 实例数是否异常增长。
  5. 对比优化前后——同一个场景,同样的操作路径,对比内存曲线。

一句话总结:内存优化不是一次性工作,而是一个持续迭代的过程。每次发版前跑一遍 Profiler,养成习惯,你的 App 会越来越稳。


公众号:蓝海资料掘金营,微信deep3321