一、什么是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 触发时机:
系统监控内存,当检测到:
- 当前设备内存吃紧(比如其他 App、后台进程、内核消耗太多)
- 或某个 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的量
- 启动次数
- (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];
});
}
}
- 获取正常退出 kill掉和正常退出都会走这个方法
func applicationWillTerminate(_ application: UIApplication) {
}
- 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 }
}
}
}
]
}