移动端内存管理概述
大家好,我是这次课程的讲师。先说说我自己的经历——早年做功能机时,内存就几十KB,大家拧着眉头算字节。到了智能手机时代,虽然内存翻了几十倍,但问题反而更棘手了。为什么?因为用户对流畅度的要求也翻了几十倍。
移动端内存管理,说白了就是在一小块有限的物理内存上,让App跑得又快又稳,还不崩溃。听起来简单?嗯,做起来全是坑。
移动端内存的独特之处
你可以先把移动设备的内存想象成一个小房间,PC是大仓库。仓库里东西多,乱一点没关系,反正空间大。小房间就不一样了,多放两件东西就转不开身。具体差异我列一下:
- 物理容量有限:当前主流手机 RAM 在 6GB~16GB,但系统、厂商预装服务就占掉一半,留给App的常常不足4GB。相比之下PC随随便便32GB起步。
- 共享与竞争:手机是前台App独占吗?不是。后台有推送服务、音乐播放、微信保活…它们都在抢同一块内存。我见过一个案例,用户后台挂了8个应用,前台的游戏直接OOM。
- 内存碎片化:频繁创建和释放小对象,会导致内存出现大量不连续的小块。你想分配一个大的连续缓冲区,系统说「有空间,但不连续」。崩溃就来了。
- GC 暂停敏感:Android的ART虚拟机和iOS的引用计数+自动释放池,都有暂停线程的时刻。哪怕只暂停几十毫秒,用户能直观感受到掉帧。
- 低内存杀进程:系统有一个Low Memory Killer(LMK),当物理内存不足时会按优先级杀掉进程。如果你的App占内存太大,还没等到GC自己,就被系统干掉了。
我遇到过哪些内存问题?
这些年排查过的线上问题,归纳起来就四类。你大概率也踩过:
- 内存泄漏:对象明明不再需要,却被某个GC Root强引用着,无法回收。最常见的是静态集合、内部类/匿名类持有Activity引用、未取消注册的Listener。我记得有一次线上OOM崩溃率飙到3%,查到最后是一个单例的HashMap里不断add View,泄漏了上百MB。
- 频繁GC引发卡顿:比如在列表快速滑动时,每帧都创建大量临时对象,导致GC不断触发。用户感受就是「滑一滑顿一下」。我曾经用systrace抓过,发现一秒内GC了8次,每次暂停30ms,帧率直接掉到20fps。
- 内存抖动(Allocation Thrashing):短时间内反复分配和释放大量对象,不仅导致GC频繁,还加剧碎片化。尤其在循环或onDraw里new对象,是大忌。
- 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在用户手里,不卡、不崩、不杀。这三个字虽然简单,但每一条背后都是成堆的代码和原理。
一个小案例,带你感受内存泄漏
我挑一个最常见的场景:在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回收不掉,不卡才怪。
本章的内容暂时就到这里。移动端内存管理是个系统工程,从特性到问题到目标,脉络很清楚。后面的章节,我会带你深入每一个环节——检测方法、GC原理、调优技巧,一个个啃下来。