本文档介绍了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
}; // 循环引用
}
@end
NSTimer(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 也强持有 timer
Delegate 代理模式中的循环引用
// 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。