本文档介绍了EMAS应用监控的内存泄漏监控,详细说明了如何使用本功能,通过全面的信息采集与分析,有效监控治理移动端的内存泄漏问题。
概述
内存泄漏是导致应用性能逐渐恶化并可能最终引发 OOM 的“隐形杀手”。EMAS 内存泄漏分析功能,通过在应用运行时智能检测生命周期本应结束但未被回收的对象,精准捕获内存泄漏事件。平台提供清晰的引用链(或引用环)分析,帮助开发者一目了然地看到是哪个对象、通过何种引用路径导致了内存无法释放,从而轻松定位并修复泄漏源头。
内存泄漏原因与表现
内存泄漏的本质是指对象不再被使用,但系统运行时无法回收,导致内存持续占用。一般表现为:
App内存使用量持续上升
可用内存不足引发App卡顿、掉帧
最终引发OOM崩溃
常见原因有:
Android
Android中造成内存泄漏原因是长生命周期对象持有短生命周期对象。下面简单列举几种常见的造成内存泄漏的场景。
静态变量持有Activity引用
public class AppUtils { // 错误:静态变量持有Activity public static Activity sCurrentActivity; } // 在Activity中 protected void onCreate() { AppUtils.sCurrentActivity = this; // 泄漏点 }非静态内部类持有外部类引用
public class MainActivity extends Activity { private Handler mHandler = new Handler() { // 匿名内部类隐式持有外部类引用 void handleMessage(Message msg) { updateUI(); // 即使Activity销毁仍可被调用 } }; void sendDelayedMessage() { mHandler.sendEmptyMessageDelayed(0, 100000); // 延迟消息导致Handler存活 } }未取消注册监听器
public class SensorActivity extends Activity { private SensorManager sensorManager; protected void onCreate() { sensorManager.registerListener(sensorListener); // 注册监听 } // 缺少onDestroy中的unregisterListener() }资源未关闭
public void loadFile() { FileInputStream fis = new FileInputStream("large.txt"); // 使用后未关闭 }
iOS
iOS 平台主要使用 ARC(Automatic Reference Counting)进行内存管理,虽能自动处理大部分引用生命周期,但仍无法完全避免循环引用(Retain Cycle) 所导致的内存泄漏问题。典型场景包括:
两个对象互设 strong 属性
@interface Child : NSObject
@property (nonatomic, strong) id parent;
@end
@interface Parent : NSObject
@property (nonatomic, strong) Child *child;
@end
Parent *p = [Parent new];
Child *c = [Child new];
p.child = c; // Parent -> Child (strong)
c.parent = p; // Child -> Parent (strong) 循环引用block 属性强引用 self
@interface MyViewModel : NSObject
@property (nonatomic, copy) void (^completion)(void);
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.viewModel.completion = ^{ // block 持有 self
[self requestFinished]; // self 也持有 viewModel
}; // 循环引用
}
@endNSTimer(scheduledTimerWithTimeInterval)
@interface MyController ()
@property (nonatomic, strong) NSTimer *timer;
@end
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self // Timer retain target
selector:@selector(tick)
userInfo:nil
repeats:YES]; // self 也强持有 timerDelegate 代理模式中的循环引用
// CustomView.h
@class CustomView;
@protocol CustomViewDelegate <NSObject>
- (void)customViewDidTap:(CustomView *)view;
@end
@interface CustomView : UIView
// 错误:delegate 属性应该总是 weak 的
@property (nonatomic, strong) id<CustomViewDelegate> delegate;
@end
// ViewController.m 中
// self.customView = [[CustomView alloc] init];
// self.customView.delegate = self; // 导致循环引用内存泄漏检测方式
Android
Android当前版本支持Java内存泄漏的检测,检测手段是分析堆转储(Heap Dumps)文件,按预置规则识别泄漏。
识别规则如下:
Activity泄漏
如果
Activity的mDestroyed为true则视为泄漏。
Service泄漏
如果
Service在ActivityThread.mServices中则视为泄漏。
Fragment泄漏
如果
Fragment的mFragmentManager为null则视为泄漏。
View泄漏(只判断根View的泄漏)
如果根
View的mContext是Activity,则Activity的mDestroyed为true则视View为泄漏。否则,如果
View已经detach,则视为泄漏。
MessageQueue泄漏
如果
MessageQueue的mQuiting为true则视为泄漏。
Window泄漏
如果
Window的mDestroyed为true则视为泄漏。
Toast泄漏
如果
Toast.mTN的mWM不为null且mView为null则视为泄漏。
iOS
为平衡检测精度与运行时性能,SDK 采用基于根对象的引用图遍历算法,在应用转入后台时自动触发检测流程,具体策略如下:
根对象类型:以
UIViewController及其子类实例作为根节点(Root Objects),因其生命周期清晰、与界面强相关,适合作为泄漏分析起点;检测时机:应用进入后台后延迟执行,避免影响前台用户体验;
遍历深度:最大检测深度为 10 层引用链,兼顾完整性与性能;
检测内容:识别对象间形成的强引用闭环,并生成可读的引用路径(如 A -> B -> C -> A)报告
循环引用检测操作为异步轻量级扫描,通常不会对主线程造成明显影响,但仍建议合理配置采样频率。
准备工作
Android
已按照Android SDK接入文档接入了内存分析。
iOS
已按照iOS SDK接入文档接入了内存分析。
功能说明
趋势分析
内存泄漏问题趋势展示筛选条件下,内存泄漏问题的波动和影响面。

指标 | 指标说明 |
问题数 | 发生内存泄漏的次数 |
影响设备数 | 发生内存泄漏的设备数量(按设备去重的问题数) |
问题率 | 问题数/设备总启动次数 |
影响设备率 | 影响设备数(按设备去重的问题数)/启动总设备数(按设备去重的启动次数) |
分布分析
分布分析支持通过多维统计(如应用版本、操作系统版本、机型等)来观测内存泄漏发生的分布情况以及定位问题。

默认以应用版本、系统版本、机型、品牌四个维度展示内存泄漏分布情况,支持点击“分布维度”按钮下拉勾选维度替换默认维度,最少选择1个,最多选择4个。
点击
可以切换视图:分布排行、列表排行。
排行分析
排行分析展示内存泄漏问题的排行情况,包含当天所有类型TOP10、当天新增类型TOP10、占比变化TOP10,帮助开发者聚焦当天变化最大的问题类型。
问题列表
问题列表展示了聚合后的内存泄漏问题类型,包括发生问题数、问题率、影响设备数、影响设备率、首现版本和问题状态。

排序方式:指标支持排序,默认按照问题数从高到低排序。
查看详情:点击问题名称,进入到对应的内存泄漏问题详情分析页。
问题详情
内存泄漏详情支持针对具体问题做详细下钻分析,提供该问题汇总的基本信息、趋势分析、分布分析和引用链/环等问题分析能力,并提供每一次客户端上报的详细信息。
基本信息

参数 | 说明 |
错误摘要 | 错误类型名称 |
聚合ID | 根据错误的特征生成的64位唯一ID |
问题数 | 问题数=所选时间段内该页面发生内存泄漏的总次数 |
问题率 | 问题率=筛选条件下问题数/筛选条件下设备总启动次数 |
影响设备数 | 影响用户数=筛选条件下发生内存泄漏的设备数量 |
影响设备率 | 影响设备率=筛选条件下影响设备数(按设备去重的问题数)/筛选条件下启动总设备数(按设备去重的启动次数) |
首次发生 | 所选时间段内首次发生的时间 |
最近发生 | 所选时间段内最近发生的时间 |
首现版本 | 首次在哪个版本出现此问题 |
统计版本 | 出现此问题统计的版本 |
标签设置 | 对此错误类型添加标签,便于管理 一个错误信息最多支持10个标签,一个标签最多显示15个字符 |
问题状态 | 问题状态支持修改,便于排查和追踪问题是否解决
|
详细信息
展示同一内存泄漏问题的所有客户端上报实例。左侧按照问题发生的时间顺序排列,点击后右侧展示此次内存泄漏的详细上报信息,包含引用链(Android)/引用环(iOS)、现场数据等。
引用链/环
引用链/环是定位内存泄漏的关键诊断区域。EMAS应用监控在安卓端提供引用链为您展示泄漏根因。当一个对象因为被某个“根对象(GC Root)”持续引用而无法释放时,平台会以层级列表的形式展示完整的引用路径。从上到下,逐层深入。最上方是“被泄漏的对象”,最下方是“GC Root”。

iOS端提供引用环,当两个或多个对象相互引用,形成一个闭环,导致它们都无法被回收时,平台会以可视化图形的方式为您展示引用环。图中的每个节点代表一个对象,箭头代表引用方向。


现场数据
现场数据还原了此条内存泄漏问题的现场信息,包括基础信息、内存信息和自定义数据。现场数据字段定义与崩溃分析的现场数据类似,请参考查看现场数据文档。
治理技巧
内存泄漏问题通常比OOM更隐蔽,但其危害不容小觑。EMAS平台通过强大的引用链(Android)和引用环(iOS)分析能力,将复杂的内存关系可视化,让开发者可以顺藤摸瓜,直达问题根源。
Android的内存泄漏通常是“单向”的:一个长生命周期的对象(GC Root)意外地持有了本该被回收的短生命周期对象。您需要做的就是找出这条“不该存在的引用链”,以如上Android引用链截图为例进行分析。
识别泄漏对象与GC Root
泄漏对象(链的顶端):查看引用链列表的最顶端,如 com.alibaba.emas.android.app.mem.MemLeakActivity。这就是本应被销毁但未能成功的对象。
GC Root(链的底端):查看列表最底部的 GC Root 标签。这通常是一个静态变量(STATIC_FIELD),是导致泄漏的“万恶之源”。
自下而上,追踪引用路径 分析引用链最有效的方法是 从下往上读,一步步看GC Root是如何“抓住”泄漏对象的:
在截图中,GC Root 是一个 StrictMode$InstanceTracker 类的静态字段 sInstances。
它持有一个 HashMap。
这个 HashMap 中的一个条目(ARRAY_ENTRY)的值,持有了 MemLeakActivity 的实例。
结论:一个静态的HashMap持有了Activity的引用,导致Activity无法被回收。
定位问题代码 关注引用链中带有下划线的 字段名,例如 sCurrentActivity、mContext 或 [key]。这正是您需要在代码中查找并修正的变量。找到持有该引用的地方,将其置为 null 或使用弱引用(WeakReference)即可切断这条引用链。
iOS的内存泄漏主要是由“循环引用”导致,即两个或多个对象通过强引用(strong reference)相互持有,形成闭环,谁也无法被释放。EMAS的可视化引用环让这个闭环一目了然,以如上iOS引用环截图为例进行分析。
识别环内成员 首先查看图中有哪些对象参与了循环,如截图中的 MemLeakBViewController 和 MemLeakCViewController。
追踪引用路径,找到闭环 沿着箭头方向追踪引用关系。每个箭头都代表一个强引用。
在截图的“循环#1”中:
MemLeakBViewController 通过 _pageCNamePath 属性强引用了 MemLeakCViewController。
同时,MemLeakCViewController 又通过 _pageBNamePath 属性强引用了 MemLeakBViewController。
结论:B和C两个ViewController通过彼此的属性形成了强引用闭环。
打破循环:将强引用改为弱引用 要解决循环引用,就必须打破这个环。通常遵循“父子”或“持有”关系原则,将其中一端的强引用(strong)修改为弱引用(weak)。
对于 Delegate 模式:delegate 属性 必须 声明为 weak。
对于 Block/闭包:在 block 内部使用 self 时,应使用 [weak self] 捕获列表,并在 block 内部使用 strongSelf 防止提前释放。
对于一般对象关系:分析业务逻辑,判断哪个对象应该“拥有”另一个。通常,子对象对父对象的引用应该是 weak 的。在上述例子中,如果B是C的父控制器,那么C对B的引用(_pageBNamePath)就应该改为 weak。