前言

在 iOS 开发中,野指针(Dangling Pointer)导致的崩溃是最难排查的问题之一。与普通崩溃不同,野指针崩溃具有以下特点:

  • 随机性强:不一定每次都崩溃,可能偶现
  • 现场迷惑:崩溃堆栈可能指向无关代码
  • 难以复现:同样的操作不一定触发

本文将从函数调用栈的底层原理出发,深入讲解野指针崩溃的产生原因、定位方法和解决思路。


一、函数调用栈基础

1.1 栈帧结构

┌─────────────────────────────────────────────────────────────────────┐
│                    函数调用栈帧结构 (ARM64)                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   高地址                                                             │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    调用者栈帧                                │   │
│   │  ┌───────────────────────────────────────────────────────┐  │   │
│   │  │  ...                                                   │  │   │
│   │  │  局部变量                                              │  │   │
│   │  │  保存的寄存器                                          │  │   │
│   │  ├───────────────────────────────────────────────────────┤  │   │
│   │  │  返回地址 (LR)           ← 调用者的下一条指令          │  │   │
│   │  │  上一个栈帧指针 (FP)     ← 指向更上层栈帧              │  │   │
│   │  └───────────────────────────────────────────────────────┘  │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              ↑                                       │
│                              │ 调用者 FP 指向这里                    │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │                    当前函数栈帧                              │   │
│   │  ┌───────────────────────────────────────────────────────┐  │   │
│   │  │  参数区 (如果参数超过8个)                              │  │   │
│   │  ├───────────────────────────────────────────────────────┤  │   │
│   │  │  局部变量                                              │  │   │
│   │  │  var1, var2, var3...                                  │  │   │
│   │  ├───────────────────────────────────────────────────────┤  │   │
│   │  │  保存的寄存器 (callee-saved)                          │  │   │
│   │  │  x19-x28, d8-d15                                      │  │   │
│   │  ├───────────────────────────────────────────────────────┤  │   │
│   │  │  返回地址 (LR/x30)                                    │  │   │
│   │  │  栈帧指针 (FP/x29)       ← 当前 FP 指向这里           │  │   │
│   │  └───────────────────────────────────────────────────────┘  │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              ↑                                       │
│                              │ SP (栈顶指针) 指向这里               │
│   低地址                                                             │
│                                                                      │
│   ════════════════════════════════════════════════════════════════  │
│                                                                      │
│   ARM64 关键寄存器:                                                 │
│   • SP (Stack Pointer): 栈顶指针,始终指向栈顶                      │
│   • FP/x29 (Frame Pointer): 栈帧指针,指向当前栈帧底部              │
│   • LR/x30 (Link Register): 返回地址,函数返回时跳转目标            │
│   • PC (Program Counter): 程序计数器,当前执行指令地址              │
│   • x0-x7: 参数传递和返回值                                         │
│   • x8: 间接返回值地址                                              │
│   • x9-x15: 临时寄存器 (caller-saved)                               │
│   • x16-x17: 过程内调用暂存                                         │
│   • x18: 平台保留                                                   │
│   • x19-x28: 保存寄存器 (callee-saved)                              │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

1.2 函数调用与返回过程

┌─────────────────────────────────────────────────────────────────────┐
│                    函数调用过程                                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   调用前 (caller):                                                   │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  1. 准备参数 (x0-x7)                                        │   │
│   │  2. 保存 caller-saved 寄存器 (如需要)                       │   │
│   │  3. BL target_function  ; 跳转并保存返回地址到 LR          │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   函数入口 (callee prologue):                                        │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ; 典型的函数序言                                           │   │
│   │  stp  x29, x30, [sp, #-16]!  ; 保存 FP 和 LR,SP -= 16     │   │
│   │  mov  x29, sp                ; 设置新的 FP                  │   │
│   │  sub  sp, sp, #N             ; 为局部变量分配空间           │   │
│   │  ; 保存 callee-saved 寄存器                                 │   │
│   │  stp  x19, x20, [sp, #offset]                               │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   函数体执行...                                                      │
│                              │                                       │
│                              ▼                                       │
│   函数返回 (callee epilogue):                                        │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  ; 典型的函数尾声                                           │   │
│   │  ; 恢复 callee-saved 寄存器                                 │   │
│   │  ldp  x19, x20, [sp, #offset]                               │   │
│   │  mov  sp, x29                ; 恢复 SP                      │   │
│   │  ldp  x29, x30, [sp], #16    ; 恢复 FP 和 LR,SP += 16     │   │
│   │  ret                         ; 跳转到 LR 指向的地址         │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

1.3 调用栈回溯原理

// 手动实现调用栈回溯
#import <mach/mach.h>
#import <dlfcn.h>

typedef struct StackFrame {
    struct StackFrame *previous;  // 上一个栈帧的 FP
    uintptr_t returnAddress;      // 返回地址 (LR)
} StackFrame;

NSArray<NSString *> *captureCallStack(void) {
    NSMutableArray *symbols = [NSMutableArray array];
    
    // 获取当前 FP
    StackFrame *frame;
    __asm__ volatile("mov %0, x29" : "=r"(frame));
    
    // 遍历栈帧链
    while (frame != NULL) {
        uintptr_t returnAddr = frame->returnAddress;
        
        // 获取符号信息
        Dl_info info;
        if (dladdr((void *)returnAddr, &info)) {
            NSString *symbol = [NSString stringWithFormat:@"%s + %lu",
                               info.dli_sname ?: "???",
                               returnAddr - (uintptr_t)info.dli_saddr];
            [symbols addObject:symbol];
        }
        
        // 移动到上一个栈帧
        frame = frame->previous;
        
        // 防止死循环
        if ((uintptr_t)frame < 0x1000 || symbols.count > 128) {
            break;
        }
    }
    
    return symbols;
}

// 使用示例
void testFunction() {
    NSArray *stack = captureCallStack();
    NSLog(@"当前调用栈:\n%@", [stack componentsJoinedByString:@"\n"]);
}

二、野指针的产生原因

2.1 什么是野指针

┌─────────────────────────────────────────────────────────────────────┐
│                    野指针产生过程                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   【正常状态】                                                       │
│                                                                      │
│   栈/堆                          堆内存                              │
│   ┌──────────────┐              ┌──────────────────┐                │
│   │ id obj       │─────────────→│   NSObject 实例   │                │
│   │ = 0x12345678 │              │   retainCount=1  │                │
│   └──────────────┘              └──────────────────┘                │
│                                                                      │
│   ════════════════════════════════════════════════════════════════  │
│                                                                      │
│   【对象被释放后 - 野指针产生】                                      │
│                                                                      │
│   栈/堆                          堆内存                              │
│   ┌──────────────┐              ┌──────────────────┐                │
│   │ id obj       │─────────────→│   ??????????     │ ← 已释放       │
│   │ = 0x12345678 │  悬垂指针!   │   可能被复用     │   内存不确定   │
│   └──────────────┘              └──────────────────┘                │
│                                                                      │
│   此时 obj 就是野指针:                                              │
│   • 指针值没变(还是 0x12345678)                                   │
│   • 但指向的内存已经无效                                            │
│   • 访问可能:崩溃/读到垃圾数据/看起来正常(最危险)               │
│                                                                      │
│   ════════════════════════════════════════════════════════════════  │
│                                                                      │
│   【内存被复用后 - 更隐蔽的问题】                                    │
│                                                                      │
│   栈/堆                          堆内存                              │
│   ┌──────────────┐              ┌──────────────────┐                │
│   │ id obj       │─────────────→│   其他对象       │ ← 被其他对象   │
│   │ = 0x12345678 │  指向错误!   │   (NSString?)    │   复用了内存   │
│   └──────────────┘              └──────────────────┘                │
│                                                                      │
│   这种情况更危险:                                                   │
│   • 通过 obj 调用方法可能调用到其他对象的方法                       │
│   • 类型不匹配可能导致随机崩溃                                      │
│   • 数据损坏可能影响其他功能                                        │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

2.2 常见野指针场景

// 场景 1: 过度释放 (Over-release)
// MRC 时代常见,ARC 下仍可能发生

id obj = [[NSObject alloc] init];
[obj release];
[obj release];  // 💥 第二次 release 导致野指针
[obj doSomething];  // 崩溃

// 场景 2: 使用 __unsafe_unretained
{
    __unsafe_unretained id unsafeObj;
    @autoreleasepool {
        unsafeObj = [[NSObject alloc] init];
    }
    // autoreleasepool 结束,对象被释放
    [unsafeObj description];  // 💥 野指针
}

// 场景 3: delegate 未置空
@interface MyView : UIView
@property (nonatomic, assign) id<MyDelegate> delegate;  // ⚠️ assign
@end

// ViewController dealloc 时
- (void)dealloc {
    // 忘记 self.myView.delegate = nil;
    // myView 可能在 dealloc 后继续回调 delegate
}

// 场景 4: 异步回调中的 self
- (void)loadData {
    [self.api fetchDataWithCompletion:^(id data) {
        // 如果 self 已经释放,这里可能崩溃
        self.data = data;  // 💥 如果 api 内部用 assign 保存 block
    }];
}

// 场景 5: 定时器持有
- (void)setupTimer {
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(tick)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)dealloc {
    // 如果忘记 invalidate,timer 继续持有 self
    // 但如果其他地方 invalidate 了,self 可能被释放
    // 而 timer 的回调在之后执行 → 野指针
}

// 场景 6: 多线程竞争
@property (nonatomic, strong) NSMutableArray *items;

// 线程 A
[self.items addObject:obj];

// 线程 B(同时执行)
self.items = nil;  // 释放了 items

// 线程 A 继续访问 items → 野指针

// 场景 7: Core Foundation 对象管理不当
CFStringRef cfStr = CFStringCreateWithCString(NULL, "test", kCFStringEncodingUTF8);
CFRelease(cfStr);
CFRelease(cfStr);  // 💥 重复释放

// 场景 8: 结构体中的对象指针
struct MyStruct {
    __unsafe_unretained id obj;  // ⚠️ 结构体中不能用 strong
};

struct MyStruct s;
s.obj = [[NSObject alloc] init];  // 对象可能随时被释放
// 稍后访问 s.obj → 可能是野指针

2.3 为什么野指针崩溃难定位

┌─────────────────────────────────────────────────────────────────────┐
│                    野指针崩溃难定位的原因                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   【原因 1: 崩溃现场与根因分离】                                     │
│                                                                      │
│   时间线:                                                           │
│   ──────────────────────────────────────────────────────────────    │
│   T1           T2              T3              T4                   │
│   创建对象     错误释放        内存被复用      访问野指针            │
│   ↓            ↓               ↓               ↓                    │
│   正常         BUG产生!        无异常          💥 崩溃              │
│                (root cause)                    (crash site)         │
│                                                                      │
│   崩溃堆栈显示 T4 的代码,但 BUG 在 T2                              │
│                                                                      │
│   ════════════════════════════════════════════════════════════════  │
│                                                                      │
│   【原因 2: 内存复用导致随机行为】                                   │
│                                                                      │
│   情况 A: 内存未被复用                                               │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │  访问已释放内存 → 读到旧数据 → 可能正常工作(假象)           │  │
│   └──────────────────────────────────────────────────────────────┘  │
│                                                                      │
│   情况 B: 内存被相同类型复用                                         │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │  访问到新对象 → 方法调用成功 → 但逻辑错误                     │  │
│   └──────────────────────────────────────────────────────────────┘  │
│                                                                      │
│   情况 C: 内存被不同类型复用                                         │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │  访问到不兼容对象 → unrecognized selector → 崩溃             │  │
│   └──────────────────────────────────────────────────────────────┘  │
│                                                                      │
│   情况 D: 内存被回收给系统                                           │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │  访问非法地址 → EXC_BAD_ACCESS → 崩溃                        │  │
│   └──────────────────────────────────────────────────────────────┘  │
│                                                                      │
│   ════════════════════════════════════════════════════════════════  │
│                                                                      │
│   【原因 3: 崩溃堆栈可能误导】                                       │
│                                                                      │
│   常见的误导性堆栈:                                                 │
│   • objc_msgSend           - 只说明在发消息,不说明发给谁           │
│   • objc_release           - 只说明在释放,不说明释放什么           │
│   • CFRelease              - 同上                                   │
│   • -[NSObject doesNotRecognizeSelector:] - 不是 bug 所在          │
│   • 系统框架内部代码       - 问题不在系统                           │
│                                                                      │
│   实际 BUG 可能在堆栈中根本看不到!                                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

三、崩溃信息解读

3.1 常见野指针崩溃类型

┌─────────────────────────────────────────────────────────────────────┐
│                    野指针相关的崩溃类型                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   【EXC_BAD_ACCESS (SIGSEGV / SIGBUS)】                             │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Exception Type: EXC_BAD_ACCESS (SIGSEGV)                   │   │
│   │  Exception Codes: KERN_INVALID_ADDRESS at 0x0000000012345678│   │
│   │                                                              │   │
│   │  含义:访问了无效的内存地址                                  │   │
│   │  原因:                                                      │   │
│   │  • 野指针访问                                                │   │
│   │  • 空指针解引用                                              │   │
│   │  • 栈溢出                                                    │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   【EXC_BAD_ACCESS (SIGBUS)】                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Exception Type: EXC_BAD_ACCESS (SIGBUS)                    │   │
│   │  Exception Codes: KERN_PROTECTION_FAILURE                   │   │
│   │                                                              │   │
│   │  含义:访问了没有权限的内存                                  │   │
│   │  原因:                                                      │   │
│   │  • 写入只读内存                                              │   │
│   │  • 对齐错误                                                  │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   【SIGABRT (abort)】                                               │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Exception Type: EXC_CRASH (SIGABRT)                        │   │
│   │  Application Specific Information:                          │   │
│   │  *** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds    │   │
│   │                                                              │   │
│   │  含义:程序主动终止(assert/exception)                      │   │
│   │  原因:                                                      │   │
│   │  • NSException 未捕获                                       │   │
│   │  • assert 失败                                               │   │
│   │  • double free                                               │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   【常见的野指针相关异常信息】                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  • unrecognized selector sent to instance 0x12345678        │   │
│   │    → 内存被其他类型对象复用                                  │   │
│   │                                                              │   │
│   │  • message sent to deallocated instance 0x12345678          │   │
│   │    → 向已释放对象发送消息(开启 Zombie 会显示)              │   │
│   │                                                              │   │
│   │  • EXC_BAD_ACCESS at objc_msgSend + 16                      │   │
│   │    → 消息发送时访问了无效的 isa 指针                        │   │
│   │                                                              │   │
│   │  • malloc: double free for ptr 0x12345678                   │   │
│   │    → 重复释放同一块内存                                      │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

3.2 解读崩溃日志

典型的野指针崩溃日志:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000102abc018
VM Region Info: 0x102abc018 is not in any region.

Triggered by Thread:  0

Thread 0 Crashed:
0   libobjc.A.dylib       0x1a2b3c4d8 objc_msgSend + 16
1   UIKitCore             0x1b2c3d4e8 -[UIView layoutSubviews] + 128
2   UIKitCore             0x1b2c3d5f4 -[UIView _layoutSubviewsIfNeeded] + 256
3   MyApp                 0x1001234f0 -[MyViewController updateUI] + 48
4   MyApp                 0x100123580 -[MyViewController viewDidAppear:] + 80
...
// 关键信息解读

// 1. 崩溃地址分析
// 0x0000000102abc018 - 看起来像有效地址(高位不全是0/F)
// 但 "not in any region" 说明这块内存已经不属于进程

// 2. objc_msgSend + 16
// 偏移 16 字节,说明崩溃在读取 isa 时
// objc_msgSend 开头会读取对象的 isa 来查找类信息
// 说明对象的内存已经失效

// 3. 需要关注的栈帧
// 第一个 MyApp 的帧:-[MyViewController updateUI]
// 这里可能是触发野指针访问的地方

// 解析崩溃地址
- (void)analyzeCrashAddress:(uintptr_t)address {
    // 检查地址特征
    if (address == 0) {
        NSLog(@"空指针解引用");
    } else if (address < 0x1000) {
        NSLog(@"可能是空对象 + 偏移访问成员变量");
    } else if (address < 0x100000000) {
        NSLog(@"可能是已释放的堆内存地址");
    } else if ((address & 0xFFFFFF0000000000) == 0) {
        NSLog(@"可能是有效但已释放的地址");
    }
    
    // 尝试获取符号信息
    Dl_info info;
    if (dladdr((void *)address, &info)) {
        NSLog(@"地址属于: %s", info.dli_fname);
    } else {
        NSLog(@"地址不属于任何已知映像");
    }
}

3.3 符号化与地址计算

// 计算符号化需要的地址

/*
崩溃日志中的地址关系:

Binary Images:
0x100000000 - 0x100ffffff MyApp arm64  <uuid> /var/.../MyApp

崩溃地址: 0x1001234f0

计算方式:
1. 文件偏移 = 崩溃地址 - 加载地址 (ASLR slide 已包含在内)
           = 0x1001234f0 - 0x100000000
           = 0x1234f0

2. 使用 atos 符号化:
   atos -arch arm64 -o MyApp.app/MyApp -l 0x100000000 0x1001234f0

3. 或计算相对偏移后使用 dSYM:
   atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp 0x1234f0
*/

// 代码中计算
void symbolicate(uintptr_t crashAddress, uintptr_t loadAddress) {
    uintptr_t offset = crashAddress - loadAddress;
    
    Dl_info info;
    if (dladdr((void *)crashAddress, &info)) {
        printf("Symbol: %s\n", info.dli_sname);
        printf("Offset: +%lu\n", crashAddress - (uintptr_t)info.dli_saddr);
    }
    
    printf("File offset: 0x%lx\n", offset);
}

四、野指针检测工具

4.1 Zombie Objects

// Zombie Objects 工作原理

/*
开启方式:
1. Xcode → Edit Scheme → Run → Diagnostics → Zombie Objects
2. 或设置环境变量 NSZombieEnabled=YES

原理:
- 对象被释放时,不真正释放内存
- 将对象的 isa 修改为 _NSZombie_OriginalClassName
- 任何对该对象的消息发送都会触发 Zombie 检测
*/

// Zombie 的实现原理(简化版)
void zombie_dealloc(id obj) {
    // 1. 获取原始类名
    Class originalClass = object_getClass(obj);
    const char *className = class_getName(originalClass);
    
    // 2. 构造 Zombie 类名
    char zombieClassName[256];
    snprintf(zombieClassName, sizeof(zombieClassName), 
             "_NSZombie_%s", className);
    
    // 3. 获取或创建 Zombie 类
    Class zombieClass = objc_lookUpClass(zombieClassName);
    if (!zombieClass) {
        zombieClass = objc_duplicateClass(
            objc_getClass("_NSZombie_"), 
            zombieClassName, 
            0
        );
    }
    
    // 4. 将对象的 isa 指向 Zombie 类
    object_setClass(obj, zombieClass);
    
    // 5. 不释放内存!
    // free(obj);  // 注释掉
}

// Zombie 对象收到消息时
void zombie_forward(id self, SEL sel) {
    Class zombieClass = object_getClass(self);
    const char *zombieName = class_getName(zombieClass);
    
    // 提取原始类名
    const char *originalName = zombieName + strlen("_NSZombie_");
    
    // 输出错误信息并崩溃
    _objc_fatal("*** -[%s %s]: message sent to deallocated instance %p",
                originalName, sel_getName(sel), self);
}

/*
Zombie 的限制:
1. 内存不会真正释放,长时间运行会 OOM
2. 只能检测 ObjC 对象
3. 只能检测消息发送,不能检测直接访问
4. Release 模式下不可用
*/

4.2 Address Sanitizer (ASan)

/*
Address Sanitizer 是编译器级别的内存检测工具

开启方式:
Xcode → Build Settings → Address Sanitizer → Yes
或
-fsanitize=address

可以检测:
• Use after free (野指针)
• Heap buffer overflow (堆溢出)
• Stack buffer overflow (栈溢出)
• Global buffer overflow (全局变量溢出)
• Use after return (返回后使用栈变量)
• Use after scope (作用域后使用)
• Memory leaks (配合 LSan)
*/

// ASan 的实现原理
/*
1. 编译时插桩
   - 每次内存访问前检查 shadow memory
   - 函数入口/出口处理栈变量

2. 运行时库
   - 替换 malloc/free
   - 管理 shadow memory
   - 在释放的内存周围放置 "红区"
   
3. Shadow Memory
   - 每 8 字节内存对应 1 字节 shadow
   - 记录内存状态:可访问/不可访问/部分可访问
*/

// 模拟 ASan 检测到的错误
void asan_example() {
    int *array = malloc(sizeof(int) * 10);
    free(array);
    
    // ASan 会检测到这里的 use-after-free
    array[0] = 42;  
    /*
    输出:
    ==12345==ERROR: AddressSanitizer: heap-use-after-free
    READ of size 4 at 0x... thread T0
        #0 0x... in asan_example file.m:123
        #1 ...
    
    0x... is located 0 bytes inside of 40-byte region
    freed by thread T0 here:
        #0 0x... in free
        #1 0x... in asan_example file.m:122
    
    previously allocated by thread T0 here:
        #0 0x... in malloc
        #1 0x... in asan_example file.m:121
    */
}

/*
ASan 的限制:
1. 性能开销大(2-5x 变慢)
2. 内存开销大(2-3x)
3. 不能检测未初始化读取(需要用 MSan)
4. 不能检测所有的内存错误
*/

4.3 Malloc Scribble

/*
Malloc Scribble 在分配和释放时填充特定值

开启方式:
环境变量 MallocScribble=1

效果:
- 分配时填充 0xAA
- 释放时填充 0x55

优势:
- 使野指针访问更容易崩溃
- 帮助识别未初始化使用
*/

// 验证 Scribble 效果
void test_scribble() {
    NSObject *obj = [[NSObject alloc] init];
    NSObject * __unsafe_unretained unsafeRef = obj;
    
    obj = nil;  // 释放对象
    
    // 如果开启了 Scribble,这里更容易崩溃
    // 因为 isa 被填充为 0x5555555555555555
    [unsafeRef description];  // 💥
}

/*
其他有用的 Malloc 调试选项:

MallocGuardEdges=1
  - 在分配的内存前后添加保护页
  - 可以检测缓冲区溢出

MallocStackLogging=1
  - 记录每次分配的调用栈
  - 可以用 malloc_history 查看

MallocStackLoggingNoCompact=1
  - 同上,但不压缩
  - 更详细但占用更多空间
*/

// 使用 malloc_history 查看分配历史
// 命令行:
// malloc_history <pid> <address>

// 代码中主动记录
#import <malloc/malloc.h>

void log_allocation_history(void *ptr) {
    // 需要开启 MallocStackLogging
    printf("Allocation at %p:\n", ptr);
    // 实际需要用 mach 接口或 malloc_history 工具
}

4.4 自定义野指针检测

// 基于 Zombie 思想的轻量级检测
// 可以在 Release 中使用(采样)

#import <objc/runtime.h>
#import <malloc/malloc.h>

// Zombie 池,保存最近释放的对象
static const NSUInteger kZombiePoolSize = 1024;
static void *sZombiePool[kZombiePoolSize];
static NSUInteger sZombieIndex = 0;
static os_unfair_lock sZombieLock = OS_UNFAIR_LOCK_INIT;

// 自定义的 Zombie 类
@interface _CustomZombie : NSObject
@property (nonatomic, assign) Class originalClass;
@property (nonatomic, strong) NSArray *deallocStack;
@end

@implementation _CustomZombie

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    NSLog(@"❌ 野指针检测!");
    NSLog(@"原始类: %@", self.originalClass);
    NSLog(@"调用方法: %@", NSStringFromSelector(invocation.selector));
    NSLog(@"释放时调用栈:\n%@", [self.deallocStack componentsJoinedByString:@"\n"]);
    NSLog(@"当前调用栈:\n%@", [NSThread callStackSymbols]);
    
    // 可以选择崩溃或只记录
    // abort();
}

@end

// Hook dealloc
static void (*original_dealloc)(id, SEL);

static void custom_dealloc(id self, SEL _cmd) {
    // 采样率控制
    static NSUInteger sampleCounter = 0;
    if (++sampleCounter % 100 != 0) {  // 1% 采样
        original_dealloc(self, _cmd);
        return;
    }
    
    // 保存原始类
    Class originalClass = object_getClass(self);
    
    // 忽略系统类
    const char *className = class_getName(originalClass);
    if (strncmp(className, "NS", 2) == 0 ||
        strncmp(className, "UI", 2) == 0 ||
        strncmp(className, "_", 1) == 0) {
        original_dealloc(self, _cmd);
        return;
    }
    
    // 创建 Zombie
    _CustomZombie *zombie = [[_CustomZombie alloc] init];
    zombie.originalClass = originalClass;
    zombie.deallocStack = [NSThread callStackSymbols];
    
    // 修改 isa
    object_setClass(self, [_CustomZombie class]);
    
    // 加入 Zombie 池(循环使用)
    os_unfair_lock_lock(&sZombieLock);
    
    // 释放老的 Zombie
    void *oldZombie = sZombiePool[sZombieIndex];
    if (oldZombie) {
        object_setClass((__bridge id)oldZombie, [NSObject class]);
        free(oldZombie);
    }
    
    // 存储新的 Zombie(不释放内存)
    sZombiePool[sZombieIndex] = (__bridge void *)self;
    sZombieIndex = (sZombieIndex + 1) % kZombiePoolSize;
    
    os_unfair_lock_unlock(&sZombieLock);
}

// 初始化
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method deallocMethod = class_getInstanceMethod([NSObject class], 
                                                        NSSelectorFromString(@"dealloc"));
        original_dealloc = (void *)method_getImplementation(deallocMethod);
        method_setImplementation(deallocMethod, (IMP)custom_dealloc);
    });
}

五、野指针定位方法论

5.1 系统化定位流程

┌─────────────────────────────────────────────────────────────────────┐
│                    野指针定位流程                                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Step 1: 收集信息                                            │   │
│   │                                                              │   │
│   │  • 崩溃日志(完整堆栈)                                      │   │
│   │  • 崩溃地址和类型                                            │   │
│   │  • 复现步骤(如果有)                                        │   │
│   │  • 崩溃用户的设备/系统版本                                   │   │
│   │  • 崩溃前的用户操作                                          │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Step 2: 初步分析                                            │   │
│   │                                                              │   │
│   │  • 符号化堆栈                                                │   │
│   │  • 分析崩溃地址特征                                          │   │
│   │  • 识别崩溃模式                                              │   │
│   │  • 定位可疑代码区域                                          │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Step 3: 尝试复现                                            │   │
│   │                                                              │   │
│   │  开启调试工具:                                              │   │
│   │  • Zombie Objects                                           │   │
│   │  • Address Sanitizer                                        │   │
│   │  • Malloc Scribble                                          │   │
│   │  • Thread Sanitizer (如果怀疑多线程)                        │   │
│   │                                                              │   │
│   │  尝试复现路径:                                              │   │
│   │  • 按用户报告的步骤                                          │   │
│   │  • 快速切换页面                                              │   │
│   │  • 频繁触发内存警告                                          │   │
│   │  • 模拟弱网络环境                                            │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│            ┌─────────────────┴─────────────────┐                    │
│            ▼                                   ▼                    │
│   ┌────────────────────┐              ┌────────────────────┐        │
│   │  能复现             │              │  不能复现           │        │
│   └────────┬───────────┘              └────────┬───────────┘        │
│            │                                   │                    │
│            ▼                                   ▼                    │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Step 4: 定位根因                                            │   │
│   │                                                              │   │
│   │  能复现时:                                                  │   │
│   │  • 用 Zombie 获取原始类名                                   │   │
│   │  • 用 ASan 获取分配/释放堆栈                                │   │
│   │  • 断点调试追踪生命周期                                      │   │
│   │                                                              │   │
│   │  不能复现时:                                                │   │
│   │  • 代码审查可疑区域                                          │   │
│   │  • 检查 delegate/block/timer 使用                           │   │
│   │  • 检查多线程安全                                            │   │
│   │  • 添加防御性代码和日志                                      │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                              │                                       │
│                              ▼                                       │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Step 5: 修复验证                                            │   │
│   │                                                              │   │
│   │  • 确认修复不引入新问题                                      │   │
│   │  • 添加单元测试                                              │   │
│   │  • 灰度验证                                                  │   │
│   │  • 监控崩溃率变化                                            │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

5.2 根据崩溃特征分类定位

┌─────────────────────────────────────────────────────────────────────┐
│                    按崩溃特征分类定位                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   【特征 1: objc_msgSend 崩溃】                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  堆栈:                                                      │   │
│   │  0 libobjc.A.dylib objc_msgSend + 16                        │   │
│   │  1 MyApp -[ViewController updateUI] + 48                    │   │
│   │                                                              │   │
│   │  分析:                                                      │   │
│   │  • +16 偏移说明在读取 isa 时崩溃                            │   │
│   │  • 对象的 isa 已经损坏或对象已释放                          │   │
│   │                                                              │   │
│   │  定位方向:                                                  │   │
│   │  • 检查 updateUI 中访问的所有对象                           │   │
│   │  • 特别关注 weak/unowned 引用                               │   │
│   │  • 检查是否有 async 回调                                    │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   【特征 2: unrecognized selector】                                  │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  异常:                                                      │   │
│   │  -[NSConcreteMutableData setFrame:]: unrecognized selector  │   │
│   │  sent to instance 0x12345678                                │   │
│   │                                                              │   │
│   │  分析:                                                      │   │
│   │  • 期望是 UIView,实际是 NSMutableData                      │   │
│   │  • 内存被不同类型的对象复用                                  │   │
│   │                                                              │   │
│   │  定位方向:                                                  │   │
│   │  • 找到调用 setFrame: 的代码                                │   │
│   │  • 检查该对象的生命周期                                      │   │
│   │  • 用 Zombie 确认原始类型                                   │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   【特征 3: objc_release 崩溃】                                      │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  堆栈:                                                      │   │
│   │  0 libobjc.A.dylib objc_release + 16                        │   │
│   │  1 MyApp -[MyObject .cxx_destruct] + 32                     │   │
│   │                                                              │   │
│   │  分析:                                                      │   │
│   │  • 对象析构时释放成员变量出错                                │   │
│   │  • 成员变量已经被错误释放过                                  │   │
│   │                                                              │   │
│   │  定位方向:                                                  │   │
│   │  • 检查 MyObject 的所有 strong 属性                         │   │
│   │  • 是否有手动内存管理代码                                    │   │
│   │  • 是否有 __bridge 转换错误                                  │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│   【特征 4: 特定地址模式】                                           │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  地址 0x0000000000000000:                                   │   │
│   │  → 空指针                                                    │   │
│   │  → 检查可选值解包                                            │   │
│   │                                                              │   │
│   │  地址 0x0000000000000010 (小地址):                          │   │
│   │  → 空对象 + 成员偏移                                        │   │
│   │  → 检查对象为 nil 时访问成员                                │   │
│   │                                                              │   │
│   │  地址 0x5555555555555555:                                   │   │
│   │  → Scribble 模式,对象已释放                                │   │
│   │  → 明确的野指针                                              │   │
│   │                                                              │   │
│   │  地址 0xDEADBEEF / 0xBADF00D:                               │   │
│   │  → 调试标记                                                  │   │
│   │  → 可能是测试代码遗留                                        │   │
│   └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

5.3 代码审查检查清单

// 野指针常见代码模式检查清单

// ❌ 模式 1: delegate 使用 assign/unsafe_unretained
@property (nonatomic, assign) id<SomeDelegate> delegate;  
// ✅ 应该用 weak
@property (nonatomic, weak) id<SomeDelegate> delegate;

// ❌ 模式 2: block 中直接使用 self
self.completionBlock = ^{
    [self doSomething];  // 循环引用或野指针
};
// ✅ 使用 weak-strong dance
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return;
    [strongSelf doSomething];
};

// ❌ 模式 3: Timer 未正确释放
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                              target:self
                                            selector:@selector(tick)
                                            userInfo:nil
                                             repeats:YES];
// ✅ 使用 block API 或 proxy
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                             repeats:YES
                                               block:^(NSTimer *timer) {
    [weakSelf tick];
}];

// ❌ 模式 4: 异步回调后直接使用成员
[self.api fetchData:^(id data) {
    self.tableView.dataSource = data;  // self 可能已释放
}];

// ❌ 模式 5: 多线程访问非线程安全对象
@property (nonatomic, strong) NSMutableArray *items;
// 线程 A: [self.items addObject:obj];
// 线程 B: [self.items removeAllObjects];
// ✅ 使用锁或串行队列
@property (nonatomic, strong) dispatch_queue_t itemsQueue;
- (void)addItem:(id)item {
    dispatch_sync(self.itemsQueue, ^{
        [self.items addObject:item];
    });
}

// ❌ 模式 6: __bridge 转换错误
CFStringRef cfStr = (__bridge CFStringRef)nsStr;
CFRelease(cfStr);  // 错误!nsStr 是 autorelease 的
// ✅ 使用正确的桥接
CFStringRef cfStr = (__bridge_retained CFStringRef)nsStr;
CFRelease(cfStr);  // OK,或者用 CFBridgingRetain

// ❌ 模式 7: 通知未移除
- (void)viewDidLoad {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handle:)
                                                 name:@"SomeNotification"
                                               object:nil];
}
// dealloc 中忘记 removeObserver
// iOS 9+ 会自动移除,但最好显式移除

// ❌ 模式 8: KVO 未移除
- (void)viewDidLoad {
    [self.model addObserver:self forKeyPath:@"value" options:0 context:nil];
}
// ✅ 确保在 dealloc 中移除
- (void)dealloc {
    [self.model removeObserver:self forKeyPath:@"value"];
}

六、高级调试技巧

6.1 LLDB 调试技巧

# 1. 在野指针崩溃时查看寄存器
(lldb) register read
General Purpose Registers:
       x0 = 0x0000000102abc018  <- 这是 self/第一个参数
       x1 = 0x00000001a2b3c4d8  <- 这是 SEL
      ...
      x29 = 0x000000016f8a5a60  <- FP
      x30 = 0x00000001001234f0  <- LR (返回地址)
       sp = 0x000000016f8a5a40  <- SP
       pc = 0x00000001a2000010  <- 当前指令

# 2. 查看对象信息
(lldb) po 0x0000000102abc018
error: Couldn't apply expression side effects : 
Couldn't read a value from memory

(lldb) memory read 0x0000000102abc018
error: memory read failed for 0x102abc018

# 3. 尝试读取 isa
(lldb) x/gx 0x0000000102abc018
0x102abc018: error: memory read failed

# 4. 如果开启了 Zombie,可能看到
(lldb) po 0x0000000102abc018
<_NSZombie_MyViewController: 0x102abc018>

# 5. 查看 selector
(lldb) po (SEL)0x00000001a2b3c4d8
"viewDidLoad"

# 6. 查看调用栈
(lldb) bt
* thread #1, queue = 'com.apple.main-thread'
  * frame #0: 0x1a2000010 libobjc.A.dylib`objc_msgSend + 16
    frame #1: 0x1001234f0 MyApp`-[ViewController updateUI] + 48

# 7. 查看特定帧的局部变量
(lldb) frame select 1
(lldb) frame variable
(MyViewController *) self = 0x0000000102abc018
(SEL) _cmd = "updateUI"

# 8. 反汇编崩溃位置
(lldb) disassemble --frame
libobjc.A.dylib`objc_msgSend:
    0x1a2000000 <+0>:  cmp    x0, #0x0
    0x1a2000004 <+4>:  b.le   0x1a2000060
    0x1a2000008 <+8>:  ldr    x13, [x0]          ; 加载 isa
->  0x1a2000010 <+16>: and    x16, x13, #0x...   ; 💥 崩溃在这里

# 9. 设置观察点监控内存
(lldb) watchpoint set expression -w write -- (void *)0x102abc018
Watchpoint created: Watchpoint 1: addr = 0x102abc018

# 10. 条件断点找野指针
(lldb) breakpoint set -n objc_msgSend -c '(void *)$x0 == 0x102abc018'

6.2 追踪对象生命周期

// 使用 Method Swizzling 追踪特定类

#import <objc/runtime.h>

@implementation LifecycleTracker

+ (void)trackClass:(Class)cls {
    // Hook alloc
    [self swizzleClassMethod:cls 
                    original:@selector(alloc) 
                    swizzled:@selector(tracked_alloc)];
    
    // Hook dealloc
    [self swizzleInstanceMethod:cls 
                       original:NSSelectorFromString(@"dealloc")
                       swizzled:@selector(tracked_dealloc)];
}

+ (void)swizzleInstanceMethod:(Class)cls original:(SEL)original swizzled:(SEL)swizzled {
    Method originalMethod = class_getInstanceMethod(cls, original);
    
    // 添加追踪方法
    IMP swizzledIMP = imp_implementationWithBlock(^(id self) {
        NSLog(@"🔴 [%@ dealloc] %p", NSStringFromClass(cls), self);
        NSLog(@"调用栈:\n%@", [NSThread callStackSymbols]);
        
        // 调用原始实现
        ((void (*)(id, SEL))method_getImplementation(originalMethod))(self, original);
    });
    
    class_replaceMethod(cls, original, swizzledIMP, method_getTypeEncoding(originalMethod));
}

+ (void)swizzleClassMethod:(Class)cls original:(SEL)original swizzled:(SEL)swizzled {
    Method originalMethod = class_getClassMethod(cls, original);
    
    IMP swizzledIMP = imp_implementationWithBlock(^id(id self) {
        id result = ((id (*)(id, SEL))method_getImplementation(originalMethod))(self, original);
        NSLog(@"🟢 [%@ alloc] %p", NSStringFromClass(cls), result);
        return result;
    });
    
    Class metaClass = object_getClass(cls);
    class_replaceMethod(metaClass, original, swizzledIMP, method_getTypeEncoding(originalMethod));
}

@end

// 使用
[LifecycleTracker trackClass:[SuspiciousClass class]];

// 输出示例:
// 🟢 [SuspiciousClass alloc] 0x12345678
// 🔴 [SuspiciousClass dealloc] 0x12345678
// 调用栈:
//   0   MyApp    -[LifecycleTracker tracked_dealloc]
//   1   MyApp    -[SomeManager cleanup]
//   2   MyApp    -[ViewController handleMemoryWarning]
//   ...

6.3 内存写入监控

// 监控特定地址的内存写入
// 使用 mach exception handler

#import <mach/mach.h>

@interface MemoryWatchdog : NSObject
+ (void)watchAddress:(void *)address size:(size_t)size;
@end

@implementation MemoryWatchdog

+ (void)watchAddress:(void *)address size:(size_t)size {
    // 设置内存保护为只读
    vm_address_t pageAddress = (vm_address_t)address & ~(PAGE_SIZE - 1);
    
    kern_return_t kr = vm_protect(
        mach_task_self(),
        pageAddress,
        PAGE_SIZE,
        FALSE,
        VM_PROT_READ  // 移除写权限
    );
    
    if (kr != KERN_SUCCESS) {
        NSLog(@"vm_protect failed: %d", kr);
        return;
    }
    
    // 设置 exception handler
    [self setupExceptionHandler];
}

+ (void)setupExceptionHandler {
    // 注册 mach exception handler
    // 当写入被保护的内存时会收到 EXC_BAD_ACCESS
    // 可以在 handler 中记录调用栈,然后恢复写权限继续执行
    
    // 这需要较复杂的 mach exception handling 代码
    // 建议使用 fishhook 或类似库
}

@end

// 更简单的方式:使用 LLDB watchpoint
// (lldb) watchpoint set expression -w write -- *(int *)0x12345678

七、预防野指针的最佳实践

7.1 编码规范

// 1. 使用正确的属性修饰符
@property (nonatomic, weak) id<Delegate> delegate;     // ✅ 代理用 weak
@property (nonatomic, copy) NSString *name;            // ✅ 字符串用 copy
@property (nonatomic, strong) NSMutableArray *items;   // ✅ 容器用 strong

// 2. Block 中的 self 处理
- (void)setupCallback {
    __weak typeof(self) weakSelf = self;
    self.callback = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (!strongSelf) return;
        
        [strongSelf doSomething];
    };
}

// 3. dealloc 中清理
- (void)dealloc {
    // 移除通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    // 停止定时器
    [self.timer invalidate];
    
    // 取消网络请求
    [self.task cancel];
    
    // 清理 delegate
    self.scrollView.delegate = nil;
    
    // 移除 KVO
    [self.model removeObserver:self forKeyPath:@"value"];
}

// 4. 多线程安全
- (void)addItem:(id)item {
    dispatch_barrier_async(self.concurrentQueue, ^{
        [self.items addObject:item];
    });
}

- (NSArray *)allItems {
    __block NSArray *result;
    dispatch_sync(self.concurrentQueue, ^{
        result = [self.items copy];
    });
    return result;
}

// 5. 避免 __unsafe_unretained
// ❌
__unsafe_unretained id unsafeRef = obj;
// ✅
__weak id weakRef = obj;

// 6. Core Foundation 正确桥接
// 创建 CF 对象并转交给 ARC
NSString *str = CFBridgingRelease(CFStringCreateWithCString(NULL, "test", kCFStringEncodingUTF8));

// 将 ARC 对象给 CF API
CFStringRef cfStr = CFBridgingRetain(nsString);
// 使用后
CFRelease(cfStr);

7.2 防御性编程

// 1. 安全的 weak-strong dance
#define weakify(obj) \
    __weak typeof(obj) weak_##obj = obj;

#define strongify(obj) \
    __strong typeof(weak_##obj) obj = weak_##obj; \
    if (!obj) return;

// 使用
- (void)loadData {
    weakify(self);
    [self.api fetchData:^(id data) {
        strongify(self);
        self.data = data;
    }];
}

// 2. 空值检查宏
#define SAFE_CALL(obj, method) \
    do { \
        id _obj = (obj); \
        if (_obj) { \
            [_obj method]; \
        } \
    } while(0)

// 3. 安全的数组访问
@implementation NSArray (Safe)
- (id)safeObjectAtIndex:(NSUInteger)index {
    if (index < self.count) {
        return self[index];
    }
    NSLog(@"⚠️ Array index out of bounds: %lu >= %lu", 
          (unsigned long)index, (unsigned long)self.count);
    return nil;
}
@end

// 4. 安全的字典访问
@implementation NSDictionary (Safe)
- (id)safeObjectForKey:(id)key {
    if (key) {
        return self[key];
    }
    NSLog(@"⚠️ Nil key passed to dictionary");
    return nil;
}
@end

// 5. 断言关键假设
- (void)processData:(NSData *)data {
    NSAssert(data != nil, @"Data should not be nil");
    NSAssert(data.length > 0, @"Data should not be empty");
    
    // 处理逻辑
}

// 6. 添加运行时检查
- (void)setDelegate:(id<MyDelegate>)delegate {
    NSAssert(delegate == nil || [delegate conformsToProtocol:@protocol(MyDelegate)],
             @"Delegate must conform to MyDelegate protocol");
    _delegate = delegate;
}

7.3 监控与告警

// 建立野指针监控系统

@interface CrashMonitor : NSObject
+ (void)setup;
@end

@implementation CrashMonitor

+ (void)setup {
    // 1. 设置未捕获异常处理
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
    
    // 2. 设置 signal 处理
    signal(SIGSEGV, signalHandler);
    signal(SIGBUS, signalHandler);
    signal(SIGABRT, signalHandler);
}

static void uncaughtExceptionHandler(NSException *exception) {
    // 记录异常信息
    NSDictionary *info = @{
        @"name": exception.name,
        @"reason": exception.reason ?: @"",
        @"stack": exception.callStackSymbols,
        @"userInfo": exception.userInfo ?: @{}
    };
    
    [self saveCrashInfo:info];
}

static void signalHandler(int signal) {
    // 获取当前堆栈
    void *callstack[128];
    int frames = backtrace(callstack, 128);
    char **symbols = backtrace_symbols(callstack, frames);
    
    NSMutableArray *stack = [NSMutableArray array];
    for (int i = 0; i < frames; i++) {
        [stack addObject:@(symbols[i])];
    }
    free(symbols);
    
    NSDictionary *info = @{
        @"signal": @(signal),
        @"stack": stack
    };
    
    [self saveCrashInfo:info];
    
    // 恢复默认处理
    signal(signal, SIG_DFL);
    raise(signal);
}

+ (void)saveCrashInfo:(NSDictionary *)info {
    // 保存到本地,下次启动时上报
    NSString *path = [self crashLogPath];
    [info writeToFile:path atomically:YES];
}

@end

八、总结

8.1 核心要点

┌─────────────────────────────────────────────────────────────────────┐
│                    野指针调试核心要点                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   【理解原理】                                                       │
│   • 野指针是指向已释放内存的指针                                    │
│   • 访问野指针可能崩溃、读到垃圾、或看似正常                        │
│   • 崩溃现场往往不是 bug 产生的地方                                 │
│                                                                      │
│   【使用工具】                                                       │
│   • Zombie Objects: 检测向已释放对象发消息                          │
│   • Address Sanitizer: 检测各种内存错误                             │
│   • Malloc Scribble: 让野指针更容易崩溃                             │
│   • LLDB: 分析崩溃现场                                              │
│                                                                      │
│   【定位方法】                                                       │
│   • 分析崩溃地址和堆栈特征                                          │
│   • 结合 Zombie 确定原始类型                                        │
│   • 追踪可疑对象的生命周期                                          │
│   • 代码审查常见模式                                                │
│                                                                      │
│   【预防措施】                                                       │
│   • 正确使用 weak/strong/unowned                                    │
│   • Block 中使用 weak-strong dance                                  │
│   • dealloc 中清理资源                                              │
│   • 多线程使用同步机制                                              │
│   • 添加防御性检查                                                  │
│                                                                      │
│   【常见陷阱】                                                       │
│   • delegate 用 assign 而不是 weak                                  │
│   • Timer 的 target 强引用问题                                      │
│   • 通知/KVO 未移除                                                 │
│   • __unsafe_unretained 的使用                                      │
│   • CF 对象桥接错误                                                 │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

8.2 调试工具速查

工具用途开启方式
Zombie Objects检测释放后使用Scheme → Diagnostics
Address Sanitizer全面内存检测Build Settings
Malloc Scribble填充已释放内存环境变量 MallocScribble=1
Malloc Guard Edges检测缓冲区溢出环境变量 MallocGuardEdges=1
Malloc Stack Logging记录分配堆栈环境变量 MallocStackLogging=1
Thread Sanitizer检测数据竞争Build Settings

参考资料

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐