移动端内存管理概述

大家好,我是这次课程的讲师。先说说我自己的经历——早年做功能机时,内存就几十KB,大家拧着眉头算字节。到了智能手机时代,虽然内存翻了几十倍,但问题反而更棘手了。为什么?因为用户对流畅度的要求也翻了几十倍。

移动端内存管理,说白了就是在一小块有限的物理内存上,让App跑得又快又稳,还不崩溃。听起来简单?嗯,做起来全是坑。

移动端内存的独特之处

你可以先把移动设备的内存想象成一个小房间,PC是大仓库。仓库里东西多,乱一点没关系,反正空间大。小房间就不一样了,多放两件东西就转不开身。具体差异我列一下:

  • 物理容量有限:当前主流手机 RAM 在 6GB~16GB,但系统、厂商预装服务就占掉一半,留给App的常常不足4GB。相比之下PC随随便便32GB起步。
  • 共享与竞争:手机是前台App独占吗?不是。后台有推送服务、音乐播放、微信保活…它们都在抢同一块内存。我见过一个案例,用户后台挂了8个应用,前台的游戏直接OOM。
  • 内存碎片化:频繁创建和释放小对象,会导致内存出现大量不连续的小块。你想分配一个大的连续缓冲区,系统说「有空间,但不连续」。崩溃就来了。
  • GC 暂停敏感:Android的ART虚拟机和iOS的引用计数+自动释放池,都有暂停线程的时刻。哪怕只暂停几十毫秒,用户能直观感受到掉帧。
  • 低内存杀进程:系统有一个Low Memory Killer(LMK),当物理内存不足时会按优先级杀掉进程。如果你的App占内存太大,还没等到GC自己,就被系统干掉了。

我遇到过哪些内存问题?

这些年排查过的线上问题,归纳起来就四类。你大概率也踩过:

  1. 内存泄漏:对象明明不再需要,却被某个GC Root强引用着,无法回收。最常见的是静态集合、内部类/匿名类持有Activity引用、未取消注册的Listener。我记得有一次线上OOM崩溃率飙到3%,查到最后是一个单例的HashMap里不断add View,泄漏了上百MB。
  2. 频繁GC引发卡顿:比如在列表快速滑动时,每帧都创建大量临时对象,导致GC不断触发。用户感受就是「滑一滑顿一下」。我曾经用systrace抓过,发现一秒内GC了8次,每次暂停30ms,帧率直接掉到20fps。
  3. 内存抖动(Allocation Thrashing):短时间内反复分配和释放大量对象,不仅导致GC频繁,还加剧碎片化。尤其在循环或onDraw里new对象,是大忌。
  4. OOM:分两种:一种是真实物理内存不足;另一种是虚拟地址空间耗尽(多见于32位进程)。Android每个App的堆大小有上限,比如早期设备Heap Growth Limit 256MB,你Bitmap一次加载几百兆就直接挂了。

优化目标究竟是什么?

我们做内存优化,不是要把内存占用降到零——那不可能。而是要在三个维度上取得平衡:

维度 具体指标 我个人的经验值
内存占用 PSS(比例集大小)不超过同类Top App的120%,GC后剩余对象数合理 控制在系统Heap Growth Limit的70%以内,留出余量给系统后台
GC频率 前台场景下,GC次数 ≤ 2次/秒,单次GC暂停 ≤ 8ms 我习惯在关键页面用Runnable去手动触发GC并统计耗时,超过15ms就要排查
稳定性 OOM崩溃率 ≤ 0.01%(万分之一),无内存泄漏导致的虚拟内存耗尽 曾经有App因为WebView泄漏,OOM率冲到0.5%,后来加了泄漏检测自动化才降下来
流畅度 帧率稳定在55fps以上,无GC导致的间歇性掉帧 用Profile GPU Rendering看,绿线超过16ms的柱子绝大多数与GC有关

说白了,优化目标就是:让你App在用户手里,不卡、不崩、不杀。这三个字虽然简单,但每一条背后都是成堆的代码和原理。

移动端内存管理知识体系 移动端内存管理核心知识体系 移动端内存管理 分配·使用·回收·监控 容量有限 物理RAM紧张 共享竞争 后台应用抢内存 碎片化 小对象分配释放 GC暂停 掉帧元凶 内存泄漏 GC Root持续引用 频繁GC 卡顿掉帧 内存抖动 瞬时分配合释放 OOM 内存溢出崩溃 优化目标:不卡·不崩·不杀 低内存占用 · 少GC暂停 · 无泄漏 · 稳定流畅 图:移动端内存管理四大特点 → 四大常见问题 → 三大优化目标,环环相扣

一个小案例,带你感受内存泄漏

我挑一个最常见的场景:在Activity里注册了一个广播接收器,却在onDestroy时忘记反注册。你想象一下:用户反复打开退出这个页面,每次都会new一个BroadcastReceiver对象,但旧的那个因为被系统内部持有引用,永远无法释放。手机内存就是这样一点一点被吃掉的。

代码长这样:

public class MyActivity extends AppCompatActivity {
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 处理网络变化
        }
    };

    @Override
    protected void onStart() {
        super.onStart();
        IntentFilter filter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE");
        registerReceiver(receiver, filter);
        // 漏掉了 unregisterReceiver!
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 这里没有 unregisterReceiver
    }
}

我曾经在一个社交App里遇到类似问题——用户反馈「切到后台再回来,App越来越卡」。用MAT一分析,发现同一个Activity的实例积了40多个,每个都带着一个BroadcastReceiver和一堆View。原因就是某个自定义View在Activity销毁时,没有取消注册一个OnGlobalLayoutListener。你想想看,40个Activity实例光布局对象就有上万,GC回收不掉,不卡才怪。

核心要点:凡是涉及注册/监听/回调的,都要成对出现。注册的地方,一定有一个地方取消注册。这是内存管理的第一条铁律。
我的排查小工具:在Android Studio中直接用Profiler看「Memory」的堆转储,然后过滤Activity类名,如果看到超过1个实例,基本就是泄漏了。配合LeakCanary可以做到自动检测。
注意:静态内部类持有外部类引用也是重灾区。匿名内部类在Lambda没出现之前,几乎每个回调都是泄漏点。如果你还在用Java写代码,记得把匿名内部类改成静态内部类,或者用WeakReference包裹。

本章的内容暂时就到这里。移动端内存管理是个系统工程,从特性到问题到目标,脉络很清楚。后面的章节,我会带你深入每一个环节——检测方法、GC原理、调优技巧,一个个啃下来。