OOM监测

202 阅读6分钟

一、什么是malloc zones

1. malloc zones 是什么

在 iOS 和 macOS 上,Apple 的 libmalloc 内存分配器并不是把所有 malloc 出来的内存都放在一个统一的堆上,而是按照策略、对象类型、分配需求等,把内存划分成多个zone(区域),每个 zone 负责管理自己那一部分内存空间。 分类: 常见的 zone 有:

  • DefaultMallocZone:普通对象、数组、字符串、UIView、ViewController等分配都在这
  • DispatchMallocZone:GCD 相关
  • VM Memory Zone:大内存块或特殊用途分配
  • MallocHelperZone
  • WebKit Malloc

2. 怎么遍历内存块

malloc_zone_t 是什么?

系统内存分配器中,每个内存分区(zone)都有一个 malloc_zone_t 结构体,结构体里有指针 introspect,指向 malloc_zone_introspect_t,这是一个 introspect 接口表,专门提供“查看/遍历当前内存块”的功能。

malloc_zone_introspect_t 定义(简化)

typedef struct {
    void (*enumerator)(task_t task, void *context, unsigned type,
                       vm_address_t zone_address, memory_reader_t reader,
                       vm_range_recorder_t recorder);
    // 其他方法...
} malloc_zone_introspect_t;

这里的 enumerator 就是“枚举当前 zone 所有内存块”的方法。


一、OOM是什么

OOM (Out Of Memory) 就是 应用因内存耗尽被系统强制杀掉 的情况。 在 iOS 上,和 Android 不同,App 被杀的原因不会直接抛异常或者日志告诉你“内存超了”,而是系统悄悄地把你杀了,而且不会在崩溃日志里留下标准的 Crash Report。

二、造成OOM崩溃的原因是什么


📌 什么是 Jetsam?

Jetsam 是 iOS / macOS 上的内存管理守护进程,名字叫 jetsam,负责:

  • 系统内存不足时,按照优先级把部分进程强制 kill 掉,释放内存。
  • 被 kill 的原因主要是内存占用超标,或者系统内存告急。 名字来源: 航海术语 jetsam,指船遇险时丢弃货物保命。

📊 Jetsam 触发时机:

系统监控内存,当检测到:

  1. 当前设备内存吃紧(比如其他 App、后台进程、内核消耗太多)
  2. 或某个 App 占用内存 > 某个阈值(阈值随机型/当前系统状态动态变化)

就会触发 jetsam,根据优先级 kill 掉部分进程。


📈 Jetsam Kill 的优先级机制:

iOS 给进程打了优先级,叫 task importance
一般规则:

  • 前台进程优先级最高(活跃 App)
  • 后台进程 / 后台任务 次之
  • 守护进程、后台音频等特殊情况单独优先级 在同等情况下,优先 kill 内存占用高、优先级低的进程。

📌 Jetsam 导致的 OOM 崩溃特点:

  • App 直接被 kill,系统 silent 杀进程,不抛 signal
  • 不触发 applicationWillTerminate:dealloc
  • 不留下任何标准崩溃日志
  • 正式版 iOS 上不会有 Jetsam 报告,只有 TestFlight / 开发版才有

👉 所以你在 Bugly/Firebase 是看不到堆栈的,只能靠异常退出数量监控 + 启动标记法侧推。

三、没有监测OOM的情况下怎么统计OOM的量

标记法:OOM的数量 = 启动次数 - crash次数 - 正常退出次数 OOM的数量 == OOM真实的量+watchDag的量

  1. 启动次数
- (void)startLaunchDetector {
    //近似作为main方法开始时间
    if (@available(iOS 13.0, *)) {
        //13.0 以上系统使用监听kCFRunLoopBeforeTimers回调作为首帧完成时机
        CFRunLoopRef mainRunLoop = [[NSRunLoop mainRunLoop] getCFRunLoop];
        CFRunLoopActivity allActivitys = kCFRunLoopAllActivities;
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, allActivitys, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            if (activity == kCFRunLoopBeforeTimers) {
                self.firstFrameEndTimeInterval = [[NSDate date] timeIntervalSince1970];
                CFRunLoopRemoveObserver(mainRunLoop, observer, kCFRunLoopCommonModes);
                
                [[NSNotificationCenter defaultCenter] postNotificationName:BMLauchFirshFrameNotification object:nil];
            }
        });
        CFRunLoopAddObserver(mainRunLoop, observer, kCFRunLoopCommonModes);
    } else {
        //13.0以下系统,使用注册block方法获取首帧完成时机
        CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
        CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){
            self.firstFrameEndTimeInterval = [[NSDate date] timeIntervalSince1970];
            [[NSNotificationCenter defaultCenter] postNotificationName:BMLauchFirshFrameNotification object:nil];
        });
    }
}
  1. 获取正常退出 kill掉和正常退出都会走这个方法
func applicationWillTerminate(_ application: UIApplication) {
}
  1. crash的数量通过bugly得到

📊 iOS App OOM 监测方案报告


📌 方案概述

为了实时监控 iOS App 运行过程中的内存占用情况,避免 OOM 崩溃,同时在超阈值时执行内存快照,辅助定位内存泄漏或异常内存波动,本文提出基于 Mach VM 内存快照、isa mask 解析和动态阈值表的 OOM 监测方案。


📦 方案核心流程


① 动态阈值表读取

目的:
根据设备型号,动态读取内存占用阈值,防止统一阈值导致误判。

实现:

NSDictionary *LoadDeviceOOMConfig(void) {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"DeviceOOMConfig" ofType:@"plist"];
    return [NSDictionary dictionaryWithContentsOfFile:path];
}

NSNumber *CurrentDeviceThreshold(void) {
    NSDictionary *thresholdConfig = LoadDeviceOOMConfig();
    NSString *model = CurrentDeviceModel();
    return thresholdConfig[model] ?: @(500 * 1024 * 1024); // 默认 500MB
}

② 子线程定时内存监测

目的:
每 2 秒检测一次 App 当前内存占用,当超阈值时触发快照。

实现:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    while (true) {
        sleep(2);
        if (currentMemoryUsage() > [CurrentDeviceThreshold() unsignedLongLongValue]) {
            [self startMemorySnapshot];
        }
    }
});

③ 多线程挂起保护

目的:
快照期间挂起除当前快照线程外的所有线程,避免内存状态变动。

实现:

thread_act_array_t threads;
mach_msg_type_number_t threadCount = 0;
task_threads(mach_task_self(), &threads, &threadCount);

thread_t currentThread = mach_thread_self();

for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
    if (threads[i] != currentThread) {
        thread_suspend(threads[i]);
    }
}
// 快照执行完再恢复
for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
    if (threads[i] != currentThread) {
        thread_resume(threads[i]);
    }
}

④ Mach VM 遍历 + isa mask 解析

目的:
遍历 malloc 区域内对象,获取 isa 指针,通过 mask 判断对象所属类。

核心快照代码:

static void range_address_callback(task_t task, void *context, unsigned type, vm_range_t *ranges, unsigned rangeCount) {
    for (unsigned int i = 0; i < rangeCount; i++) {
        vm_range_t range = ranges[i];
        flex_maybe_object_t *object = (flex_maybe_object_t *)range.address;
        Class cls = nil;

#ifdef __arm64__
        extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;
        cls = (__bridge Class)((void *)(((uint64_t)object->isa) & objc_debug_isa_class_mask));
#else
        cls = object->isa;
#endif
       
        if (CFSetContainsValue(allClasses, (__bridge const void *)(cls))) {
            // 记录 className, address, size
            NSString *className = NSStringFromClass(cls);
            NSString *addressHex = AddressToHex(range.address);
            NSInteger existingCount = [resultDic[className][@"count"] integerValue];

            if (existingCount > 20) continue; // 限量保留

            // 引用对象递归
            NSMutableDictionary *subObjects = collectSubObjects(range);

            NSInteger totalSize = [resultDic[className][@"size"] integerValue];
            resultDic[className] = @{
                @"count": @(existingCount + 1),
                @"size": @(totalSize + range.size),
                @"data": @{
                    addressHex: @{
                        @"size": @(range.size),
                        @"subs": subObjects
                    }
                }
            };
        }
    }
}

⑤ 深度递归子对象引用

目的:
在快照时递归分析对象内成员变量/引用对象。

实现:

NSMutableDictionary *collectSubObjects(vm_range_t range) {
    NSMutableDictionary *subObjects = [NSMutableDictionary dictionary];
    vm_address_t subs = range.address;
    uint64_t endAddr = range.address + range.size;

    while (subs < endAddr) {
        flex_maybe_id_object_t *subObj = (flex_maybe_id_object_t *)subs;
        uint64_t quotedAddr = (uint64_t)subObj->isa;

#ifdef __arm64__
        extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;
        quotedAddr = quotedAddr & objc_debug_isa_class_mask;
#endif

        if (quotedAddr > 0 && isAnObject((void *)quotedAddr)) {
            uintptr_t isa = (*(uintptr_t *)quotedAddr);
            Class qCls = (__bridge Class)((void *)(isa & objc_debug_isa_class_mask));
            if (qCls) {
                NSString *className = NSStringFromClass(qCls);
                NSString *addrStr = AddressToHex(quotedAddr);
                subObjects[className] = @{ addrStr: @{ @"size": @(malloc_size((void *)quotedAddr)) } };
            }
        }
        subs += 8;
    }
    return subObjects;
}

⑥ 数据上报结构

最终快照数据:

{
  "timestamp": 1713284044,
  "memory_usage": "512MB",
  "threshold": "500MB",
  "snapshots": [
    {
      "class": "MyViewController",
      "address": "0x1234abcd",
      "size": 4096,
      "subs": {
        "UIButton": {
          "0x4567ef12": { "size": 512 }
        }
      }
    }
  ]
}