内存泄漏

本文档介绍了EMAS应用监控的内存泄漏监控,详细说明了如何使用本功能,通过全面的信息采集与分析,有效监控治理移动端的内存泄漏问题。

概述

内存泄漏是导致应用性能逐渐恶化并可能最终引发 OOM 的“隐形杀手”。EMAS 内存泄漏分析功能,通过在应用运行时智能检测生命周期本应结束但未被回收的对象,精准捕获内存泄漏事件。平台提供清晰的引用链(或引用环)分析,帮助开发者一目了然地看到是哪个对象、通过何种引用路径导致了内存无法释放,从而轻松定位并修复泄漏源头。

内存泄漏原因与表现

内存泄漏的本质是指对象不再被使用,但系统运行时无法回收,导致内存持续占用。一般表现为:

  • App内存使用量持续上升

  • 可用内存不足引发App卡顿、掉帧

  • 最终引发OOM崩溃

常见原因有:

Android

Android中造成内存泄漏原因是长生命周期对象持有短生命周期对象。下面简单列举几种常见的造成内存泄漏的场景。

  1. 静态变量持有Activity引用

    public class AppUtils {
        // 错误:静态变量持有Activity
        public static Activity sCurrentActivity;
    }
    
    // 在Activity中
    protected void onCreate() {
        AppUtils.sCurrentActivity = this; // 泄漏点
    }
    
  2. 非静态内部类持有外部类引用

    public class MainActivity extends Activity {
        private Handler mHandler = new Handler() { // 匿名内部类隐式持有外部类引用
            void handleMessage(Message msg) {
                updateUI(); // 即使Activity销毁仍可被调用
            }
        };
    
        void sendDelayedMessage() {
            mHandler.sendEmptyMessageDelayed(0, 100000); // 延迟消息导致Handler存活
        }
    }
    
  3. 未取消注册监听器

    public class SensorActivity extends Activity {
        private SensorManager sensorManager;
    
        protected void onCreate() {
            sensorManager.registerListener(sensorListener); // 注册监听
        }
    
        // 缺少onDestroy中的unregisterListener()
    }
  4. 资源未关闭

    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泄漏

    • 如果ActivitymDestroyedtrue则视为泄漏。

  • Service泄漏

    • 如果ServiceActivityThread.mServices中则视为泄漏。

  • Fragment泄漏

    • 如果FragmentmFragmentManagernull则视为泄漏。

  • View泄漏(只判断根View的泄漏)

    • 如果根ViewmContextActivity,则ActivitymDestroyedtrue则视View为泄漏。

    • 否则,如果View已经detach,则视为泄漏。

  • MessageQueue泄漏

    • 如果MessageQueuemQuitingtrue则视为泄漏。

  • Window泄漏

    • 如果WindowmDestroyedtrue则视为泄漏。

  • Toast泄漏

    • 如果Toast.mTNmWM不为nullmViewnull则视为泄漏。

iOS

为平衡检测精度与运行时性能,SDK 采用基于根对象的引用图遍历算法,在应用转入后台时自动触发检测流程,具体策略如下:

  • 根对象类型:以 UIViewController 及其子类实例作为根节点(Root Objects),因其生命周期清晰、与界面强相关,适合作为泄漏分析起点;

  • 检测时机:应用进入后台后延迟执行,避免影响前台用户体验;

  • 遍历深度:最大检测深度为 10 层引用链,兼顾完整性与性能;

  • 检测内容:识别对象间形成的强引用闭环,并生成可读的引用路径(如 A -> B -> C -> A)报告

说明

循环引用检测操作为异步轻量级扫描,通常不会对主线程造成明显影响,但仍建议合理配置采样频率。

准备工作

Android

已按照Android SDK接入文档接入了内存分析。

iOS

已按照iOS SDK接入文档接入了内存分析。

功能说明

趋势分析

内存泄漏问题趋势展示筛选条件下,内存泄漏问题的波动和影响面。

image

指标

指标说明

问题数

发生内存泄漏的次数

影响设备数

发生内存泄漏的设备数量(按设备去重的问题数)

问题率

问题数/设备总启动次数

影响设备率

影响设备数(按设备去重的问题数)/启动总设备数(按设备去重的启动次数)

分布分析

分布分析支持通过多维统计(如应用版本、操作系统版本、机型等)来观测内存泄漏发生的分布情况以及定位问题。

image

  • 默认以应用版本、系统版本、机型、品牌四个维度展示内存泄漏分布情况,支持点击“分布维度”按钮下拉勾选维度替换默认维度,最少选择1个,最多选择4个。

  • 点击image.png可以切换视图:分布排行、列表排行。

排行分析

排行分析展示内存泄漏问题的排行情况,包含当天所有类型TOP10、当天新增类型TOP10、占比变化TOP10,帮助开发者聚焦当天变化最大的问题类型。

问题列表

问题列表展示了聚合后的内存泄漏问题类型,包括发生问题数、问题率、影响设备数、影响设备率、首现版本和问题状态。

image

  • 排序方式:指标支持排序,默认按照问题数从高到低排序。

  • 查看详情:点击问题名称,进入到对应的内存泄漏问题详情分析页。

问题详情

内存泄漏详情支持针对具体问题做详细下钻分析,提供该问题汇总的基本信息、趋势分析、分布分析和引用链/环等问题分析能力,并提供每一次客户端上报的详细信息。

基本信息

image

参数

说明

错误摘要

错误类型名称

聚合ID

根据错误的特征生成的64位唯一ID

问题数

问题数=所选时间段内该页面发生内存泄漏的总次数

问题率

问题率=筛选条件下问题数/筛选条件下设备总启动次数

影响设备数

影响用户数=筛选条件下发生内存泄漏的设备数量

影响设备率

影响设备率=筛选条件下影响设备数(按设备去重的问题数)/筛选条件下启动总设备数(按设备去重的启动次数)

首次发生

所选时间段内首次发生的时间

最近发生

所选时间段内最近发生的时间

首现版本

首次在哪个版本出现此问题

统计版本

出现此问题统计的版本

标签设置

对此错误类型添加标签,便于管理

一个错误信息最多支持10个标签,一个标签最多显示15个字符

问题状态

问题状态支持修改,便于排查和追踪问题是否解决

  • NEW:新出现

  • FIXED:已被修复

  • OPEN:被修复后再次出现

  • CLOSE:已被关闭

详细信息

展示同一内存泄漏问题的所有客户端上报实例。左侧按照问题发生的时间顺序排列,点击后右侧展示此次内存泄漏的详细上报信息,包含引用链(Android)/引用环(iOS)、现场数据等。

引用链/环

引用链/环是定位内存泄漏的关键诊断区域。EMAS应用监控在安卓端提供引用链为您展示泄漏根因。当一个对象因为被某个“根对象(GC Root)”持续引用而无法释放时,平台会以层级列表的形式展示完整的引用路径。从上到下,逐层深入。最上方是“被泄漏的对象”,最下方是“GC Root”。

image

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

imageimage

现场数据

现场数据还原了此条内存泄漏问题的现场信息,包括基础信息、内存信息和自定义数据。现场数据字段定义与崩溃分析的现场数据类似,请参考查看现场数据文档。

治理技巧

内存泄漏问题通常比OOM更隐蔽,但其危害不容小觑。EMAS平台通过强大的引用链(Android)引用环(iOS)分析能力,将复杂的内存关系可视化,让开发者可以顺藤摸瓜,直达问题根源。

Android的内存泄漏通常是“单向”的:一个长生命周期的对象(GC Root)意外地持有了本该被回收的短生命周期对象。您需要做的就是找出这条“不该存在的引用链”,以如上Android引用链截图为例进行分析。

  1. 识别泄漏对象与GC Root

    • 泄漏对象(链的顶端):查看引用链列表的最顶端,如 com.alibaba.emas.android.app.mem.MemLeakActivity。这就是本应被销毁但未能成功的对象。

    • GC Root(链的底端):查看列表最底部的 GC Root 标签。这通常是一个静态变量(STATIC_FIELD),是导致泄漏的“万恶之源”。

  2. 自下而上,追踪引用路径 分析引用链最有效的方法是 从下往上读,一步步看GC Root是如何“抓住”泄漏对象的:

    • 在截图中,GC Root 是一个 StrictMode$InstanceTracker 类的静态字段 sInstances。

    • 它持有一个 HashMap。

    • 这个 HashMap 中的一个条目(ARRAY_ENTRY)的值,持有了 MemLeakActivity 的实例。

    • 结论:一个静态的HashMap持有了Activity的引用,导致Activity无法被回收。

  3. 定位问题代码 关注引用链中带有下划线的 字段名,例如 sCurrentActivity、mContext 或 [key]。这正是您需要在代码中查找并修正的变量。找到持有该引用的地方,将其置为 null 或使用弱引用(WeakReference)即可切断这条引用链。

iOS的内存泄漏主要是由“循环引用”导致,即两个或多个对象通过强引用(strong reference)相互持有,形成闭环,谁也无法被释放。EMAS的可视化引用环让这个闭环一目了然,以如上iOS引用环截图为例进行分析。

  1. 识别环内成员 首先查看图中有哪些对象参与了循环,如截图中的 MemLeakBViewController 和 MemLeakCViewController。

  2. 追踪引用路径,找到闭环 沿着箭头方向追踪引用关系。每个箭头都代表一个强引用。

    • 在截图的“循环#1”中:

      • MemLeakBViewController 通过 _pageCNamePath 属性强引用了 MemLeakCViewController。

      • 同时,MemLeakCViewController 又通过 _pageBNamePath 属性强引用了 MemLeakBViewController。

    • 结论:BC两个ViewController通过彼此的属性形成了强引用闭环。

  3. 打破循环:将强引用改为弱引用 要解决循环引用,就必须打破这个环。通常遵循“父子”或“持有”关系原则,将其中一端的强引用(strong)修改为弱引用(weak)。

    • 对于 Delegate 模式:delegate 属性 必须 声明为 weak。

    • 对于 Block/闭包:在 block 内部使用 self 时,应使用 [weak self] 捕获列表,并在 block 内部使用 strongSelf 防止提前释放。

    • 对于一般对象关系:分析业务逻辑,判断哪个对象应该“拥有”另一个。通常,子对象对父对象的引用应该是 weak 的。在上述例子中,如果BC的父控制器,那么CB的引用(_pageBNamePath)就应该改为 weak。