# iOS 稳定性方向常见面试题与详解


目录

  1. [Crash 分类与原理](#Crash 分类与原理 "#1-crash-%E5%88%86%E7%B1%BB%E4%B8%8E%E5%8E%9F%E7%90%86")
  2. [Mach 异常与 Unix Signal](#Mach 异常与 Unix Signal "#2-mach-%E5%BC%82%E5%B8%B8%E4%B8%8E-unix-signal")
  3. [OC 层异常捕获(NSException)](#OC 层异常捕获(NSException) "#3-oc-%E5%B1%82%E5%BC%82%E5%B8%B8%E6%8D%95%E8%8E%B7nsexception")
  4. [Unrecognized Selector 防护](#Unrecognized Selector 防护 "#4-unrecognized-selector-%E9%98%B2%E6%8A%A4")
  5. [KVO Crash 防护](#KVO Crash 防护 "#5-kvo-crash-%E9%98%B2%E6%8A%A4")
  6. [容器类越界/插 nil 防护](#容器类越界/插 nil 防护 "#6-%E5%AE%B9%E5%99%A8%E7%B1%BB%E8%B6%8A%E7%95%8C%E6%8F%92-nil-%E9%98%B2%E6%8A%A4")
  7. [野指针 Crash](#野指针 Crash "#7-%E9%87%8E%E6%8C%87%E9%92%88-crash")
  8. [内存管理与 OOM](#内存管理与 OOM "#8-%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E4%B8%8E-oom")
  9. [Watchdog 超时(0x8badf00d)](#Watchdog 超时(0x8badf00d) "#9-watchdog-%E8%B6%85%E6%97%B60x8badf00d")
  10. 后台任务被系统杀死
  11. [Method Swizzling 在稳定性防护中的应用](#Method Swizzling 在稳定性防护中的应用 "#11-method-swizzling-%E5%9C%A8%E7%A8%B3%E5%AE%9A%E6%80%A7%E9%98%B2%E6%8A%A4%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8")
  12. [符号化与 Crash 日志分析](#符号化与 Crash 日志分析 "#12-%E7%AC%A6%E5%8F%B7%E5%8C%96%E4%B8%8E-crash-%E6%97%A5%E5%BF%97%E5%88%86%E6%9E%90")
  13. [APM 与 Crash 监控体系搭建](#APM 与 Crash 监控体系搭建 "#13-apm-%E4%B8%8E-crash-%E7%9B%91%E6%8E%A7%E4%BD%93%E7%B3%BB%E6%90%AD%E5%BB%BA")
  14. [多线程 Crash](#多线程 Crash "#14-%E5%A4%9A%E7%BA%BF%E7%A8%8B-crash")
  15. [Fishhook 与系统函数 Hook](#Fishhook 与系统函数 Hook "#15-fishhook-%E4%B8%8E%E7%B3%BB%E7%BB%9F%E5%87%BD%E6%95%B0-hook")
  16. [线上 Crash 率治理实践](#线上 Crash 率治理实践 "#16-%E7%BA%BF%E4%B8%8A-crash-%E7%8E%87%E6%B2%BB%E7%90%86%E5%AE%9E%E8%B7%B5")
  17. [启动阶段 Crash 的特殊处理](#启动阶段 Crash 的特殊处理 "#17-%E5%90%AF%E5%8A%A8%E9%98%B6%E6%AE%B5-crash-%E7%9A%84%E7%89%B9%E6%AE%8A%E5%A4%84%E7%90%86")
  18. 堆栈回溯原理
  19. 卡顿监控与治理
  20. 磁盘与数据库稳定性

1. Crash 分类与原理

Q: iOS Crash 可以分为哪几大类?各自的产生原因是什么?

iOS Crash 主要分为三大类:

(1) OC 异常(NSException)

由 Objective-C 运行时或 Foundation 框架抛出,常见场景:

  • unrecognized selector sent to instance:对象收到了未实现的消息
  • 数组越界 objectAtIndex: 超出 bounds
  • 字典插入 nil value 或 nil key
  • KVO 移除了未注册的观察者
  • NSInvalidArgumentException:参数不合法

(2) Mach 异常 / Unix Signal

操作系统层面的异常,由内核产生:

Signal 含义 常见场景
SIGSEGV 访问无效内存 野指针、访问已释放对象
SIGBUS 总线错误,内存对齐问题 访问未映射地址
SIGABRT 程序主动调用 abort() NSException 未捕获会触发
SIGTRAP 断点/陷阱指令 __builtin_trap()、Swift fatalError
SIGILL 非法指令 代码段损坏
SIGFPE 算术异常 除零
SIGPIPE 向已关闭的 socket 写数据 网络编程

(3) 被系统杀死

不属于传统意义的 Crash,但表现一致:

  • Watchdog(0x8badf00d):主线程卡死超时
  • Jetsam/OOM(0xd00d2bad):内存超限被系统终止
  • 后台超时:后台任务未在限定时间内完成

2. Mach 异常与 Unix Signal

Q: 请描述 Mach 异常和 Unix Signal 的关系和传递流程。

传递流程

arduino 复制代码
硬件异常 / 软件异常
      ↓
  Mach 异常(内核态)
      ↓
  Mach 异常处理 port(task/thread/host 级别)
      ↓
  如果未处理,内核将其转换为对应的 Unix Signal
      ↓
  Signal Handler(用户态)
      ↓
  如果未处理,进程被终止

关键要点

  1. Mach 异常先于 Signal:内核先尝试通过 Mach 异常端口投递,如果没有 handler 消化,才转换成 Signal。
  2. 注册 Mach 异常 handler :通过 task_set_exception_ports() 在 task 级别注册,或通过 thread_set_exception_ports() 在线程级别注册。
  3. 注册 Signal handler :通过 signal() 或更推荐的 sigaction() 注册。
  4. 两者都注册时:先触发 Mach 异常 handler,再触发 Signal handler。
  5. NSException 最终也会走到 Signal :未被 @try-@catchNSSetUncaughtExceptionHandler 捕获的 OC 异常,最终调用 abort() 产生 SIGABRT

Mach 异常与 Signal 的映射

Mach 异常 Unix Signal
EXC_BAD_ACCESS SIGSEGV / SIGBUS
EXC_BAD_INSTRUCTION SIGILL
EXC_ARITHMETIC SIGFPE
EXC_BREAKPOINT SIGTRAP
EXC_SOFTWARE SIGABRT / SIGPIPE 等

为什么 Crash 收集框架同时注册两者?

  • Mach 异常可以获得更底层的信息(如 fault address),但不是所有异常都先经过 Mach(如 abort() 直接发 signal)。
  • Signal handler 可以兜底,但在某些场景下信息不完整。
  • 同时注册可以覆盖更全面的 Crash 场景。

3. OC 层异常捕获(NSException)

Q: NSSetUncaughtExceptionHandler 的原理和注意事项是什么?

原理

objectivec 复制代码
void MyUncaughtExceptionHandler(NSException *exception) {
    NSString *name = exception.name;
    NSString *reason = exception.reason;
    NSArray *callStack = exception.callStackSymbols;
    // 持久化保存 crash 信息
}

// 注册
NSSetUncaughtExceptionHandler(&MyUncaughtExceptionHandler);
  • OC 运行时在异常未被 @try-@catch 捕获时,检查是否设置了全局的 UncaughtExceptionHandler。
  • 如果有,调用该 handler,传入 NSException 对象。
  • handler 返回后,运行时调用 abort() 终止进程。

注意事项

  1. Handler 会被覆盖 :多个 SDK 都可能调用 NSSetUncaughtExceptionHandler,后者覆盖前者。正确做法是保存前一个 handler 并在自己的 handler 中转发:
objectivec 复制代码
static NSUncaughtExceptionHandler *previousHandler = nil;

void MyHandler(NSException *exception) {
    // 自己的处理逻辑
    saveException(exception);
    // 转发给前一个 handler
    if (previousHandler) {
        previousHandler(exception);
    }
}

previousHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&MyHandler);
  1. handler 中不能做太多事:此时进程即将终止,信号可能不安全,应避免申请大块内存、使用锁等操作。
  2. 只能捕获 OC 异常:C++ 异常、Mach 异常等无法通过此方式捕获。
  3. callStackSymbols 不一定完整:Release 环境下符号可能被 strip,需要配合 dSYM 符号化。

4. Unrecognized Selector 防护

Q: 如何在线上防护 unrecognized selector 导致的 Crash?请描述 OC 消息转发机制。

OC 消息转发三步流程

objectivec 复制代码
1. 动态方法决议(Dynamic Method Resolution)
   +resolveInstanceMethod: / +resolveClassMethod:
   → 可以动态添加方法实现
          ↓ 返回 NO
2. 快速转发(Fast Forwarding)
   -forwardingTargetForSelector:
   → 返回另一个可以处理该消息的对象
          ↓ 返回 nil
3. 完整转发(Normal Forwarding)
   -methodSignatureForSelector:
   -forwardInvocation:
   → 构造 NSInvocation 做完整转发
          ↓ 未处理
   调用 -doesNotRecognizeSelector: → 抛出 NSException → Crash

防护方案(以 HSSafeKit 为例)

Hook forwardingTargetForSelector: 方法,在消息转发的第二步进行拦截:

objectivec 复制代码
- (id)HSSafeKit_forwardingTargetForSelector:(SEL)selector {
    BOOL aBool = [self respondsToSelector:@selector(selector)];
    NSMethodSignature *signature = [self methodSignatureForSelector:selector];
    // 如果已经有消息转发的实现(如 JSPatch),不拦截,继续走原始流程
    if (aBool || signature) {
        return [self HSSafeKit_forwardingTargetForSelector:selector];
    } else {
        // 报告异常但不 crash,返回一个动态添加了该方法的"桩对象"
        reportBug(self.class, selector);
        HSSafeKitObj *safeKitObj = [[HSSafeKitObj alloc] init];
        [safeKitObj addAnyFunc:selector];
        return safeKitObj;
    }
}

关键设计细节

  1. 桩对象动态添加方法 :通过 class_addMethod 给桩对象添加空实现,避免继续走消息转发导致死循环。
  2. 兼容性考量 :必须判断 methodSignatureForSelector: 是否返回非 nil,因为有些框架(如 JSPatch、Aspects)依赖完整消息转发,如果提前拦截会导致这些框架失效。
  3. 上报但不 crash:线上记录异常信息用于排查,但不中断用户体验。

5. KVO Crash 防护

Q: KVO 常见的 Crash 场景有哪些?如何防护?

常见 Crash 场景

  1. 移除未注册的观察者-removeObserver:forKeyPath: 找不到匹配的注册信息
  2. 重复添加同一观察者(某些场景会导致回调多次触发或移除时 crash)
  3. 被观察对象 dealloc 时仍有观察者注册
  4. 观察者 dealloc 时未移除注册(iOS 11 以前会 crash)

防护方案:KVO 代理中间层

核心思想:维护一个 KVO 关系映射表,拦截 add/remove 操作。

objectivec 复制代码
@interface KVOProxy : NSObject
@property (nonatomic, strong) NSMapTable<NSString *, NSHashTable *> *observerMap;
@end

// Hook addObserver:forKeyPath:options:context:
- (void)safe_addObserver:(NSObject *)observer
              forKeyPath:(NSString *)keyPath
                 options:(NSKeyValueObservingOptions)options
                 context:(void *)context {
    // 检查是否已注册,防止重复
    if ([self.kvoProxy containsObserver:observer forKeyPath:keyPath]) {
        return;
    }
    [self.kvoProxy recordObserver:observer forKeyPath:keyPath];
    [self safe_addObserver:observer forKeyPath:keyPath options:options context:context];
}

// Hook removeObserver:forKeyPath:
- (void)safe_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    if (![self.kvoProxy containsObserver:observer forKeyPath:keyPath]) {
        return; // 未注册则跳过,避免 crash
    }
    [self.kvoProxy removeObserver:observer forKeyPath:keyPath];
    [self safe_removeObserver:observer forKeyPath:keyPath];
}

注意事项

  • Hook 方式有两种:(a) Swizzle NSObject 的 KVO 方法;(b) 使用 KVO 代理对象集中管理。
  • 方案 (b) 更安全,但实现复杂度更高。
  • 需要在被观察对象 dealloc 时自动清理未移除的观察者。

6. 容器类越界/插 nil 防护

Q: 如何对 NSArray/NSMutableArray/NSDictionary 等容器类做安全防护?为什么需要 Hook 类簇的真实子类?

为什么需要 Hook 真实子类?

OC 中的 NSArrayNSDictionary 等是**类簇(Class Cluster)**设计模式,对外暴露抽象接口,内部使用不同的私有子类:

抽象类 真实子类
NSArray __NSArrayI(不可变)、__NSArray0(空数组)、__NSSingleObjectArrayI(单元素)
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM
NSString __NSCFStringNSTaggedPointerString

直接 Swizzle NSArray 的方法不会生效,因为实际运行时对象的类是 __NSArrayI。必须通过 NSClassFromString() 获取真实子类再进行 Swizzle。

防护示例

objectivec 复制代码
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = NSClassFromString(@"__NSArrayI");
        safeKit_swizzleSelector(class,
            @selector(objectAtIndex:), self,
            @selector(HSSafeKit_objectAtIndex:));
    });
}

- (id)HSSafeKit_objectAtIndex:(NSUInteger)index {
    if (index >= self.count) {
        // 记录并上报,但不 crash
        reportException(@"NSArray index out of bounds");
        return nil;
    }
    return [self HSSafeKit_objectAtIndex:index];
}

常见防护点

  • NSArray: objectAtIndex:, objectAtIndexedSubscript:, initWithObjects:count:
  • NSMutableArray: addObject:, insertObject:atIndex:, removeObjectAtIndex:, replaceObjectAtIndex:withObject:
  • NSDictionary: initWithObjects:forKeys:count:
  • NSMutableDictionary: setObject:forKey:, removeObjectForKey:
  • NSString: substringWithRange:, characterAtIndex:, stringWithUTF8String:(NULL 检查)

7. 野指针 Crash

Q: 什么是野指针?为什么野指针 Crash 难以复现?如何检测和防护?

什么是野指针

对象被释放后,指向该内存地址的指针没有被置 nil,再次访问就是野指针访问。

为什么难以复现

对象释放后内存进入空闲池,如果该内存被立即复用(分配给新对象),访问旧指针可能:

  • 刚好访问到新对象 → 行为不可预期但不一定 crash
  • 内存未被复用 → SIGSEGV crash
  • 内存被复用为相同类型对象 → 可能表现正常

这种随机性导致问题难以复现。

检测手段

1. Xcode Zombie Objects(开发阶段)

开启 NSZombieEnabled,对象释放后不真正回收内存,而是变成"僵尸对象"。再次访问时会打印明确的日志并 crash。

原理:

  • Swizzle dealloc,对象释放时将 isa 指针指向 _NSZombie_OriginalClass
  • 向僵尸对象发消息时,objc_msgSend 识别到 Zombie 类,打印日志

2. Address Sanitizer (ASan)

编译时插桩,运行时检测内存越界、use-after-free 等。性能开销约 2-5x,适合开发和测试阶段。

3. Malloc Scribble

释放内存时用 0x55 填充,访问已释放内存大概率 crash,提高复现率。

4. 线上野指针防护方案

思路类似 Zombie,但更轻量:

markdown 复制代码
对象 dealloc 时:
1. 不立即释放内存,放入一个延迟释放队列
2. 将 isa 替换为代理类,代理类的所有方法都上报异常
3. 延迟释放队列满(如超过 10MB)时,真正释放最早入队的对象

这样在对象被真正回收前,任何野指针访问都会被代理类拦截。


8. 内存管理与 OOM

Q: 什么是 OOM?Jetsam 机制是什么?如何监控和治理 OOM?

OOM(Out Of Memory)

iOS 没有内存交换机制(Swap),物理内存耗尽时系统直接终止进程。App 被 Jetsam 杀死时不会产生标准 Crash 日志。

Jetsam 机制

Jetsam 是 iOS/macOS 的内存压力管理系统:

  1. 系统维护一个进程优先级队列
  2. 内存压力升高时按优先级从低到高终止进程
  3. 优先级:后台App < 前台App < 系统进程
  4. 终止原因码 0xd00d2bad(FC_RES_TYPE_MEMORY)

内存警告级别

objectivec 复制代码
UIApplicationDidReceiveMemoryWarningNotification
  ↓
didReceiveMemoryWarning
  ↓
如果内存未降低 → Jetsam 终止进程

如何监控 OOM

1. 排除法判断 OOM

Facebook 提出的方案:如果上次退出不是以下原因,则认为是 OOM:

diff 复制代码
非 OOM 退出原因:
- 用户主动杀进程
- App 更新
- crash(有 crash 日志)
- watchdog 杀死
- 正常退出(applicationWillTerminate)
- 调试模式下被 Xcode 杀死

排除以上所有原因后 → 判定为 OOM

2. 内存水位监控

定期采样 App 内存占用:

objectivec 复制代码
#import <mach/mach.h>

- (int64_t)memoryUsageInBytes {
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t result = task_info(mach_task_self(),
                                     TASK_VM_INFO,
                                     (task_info_t)&vmInfo,
                                     &count);
    if (result == KERN_SUCCESS) {
        return vmInfo.phys_footprint; // 物理内存占用
    }
    return 0;
}

OOM 治理方向

方向 措施
图片 使用 ImageIO 降采样加载,避免全尺寸解码;及时释放不可见图片
缓存 使用 NSCache(自动响应内存警告),设置合理上限
大数据 分页加载,避免一次加载全量数据
WebView WKWebView 独立进程,OOM 不影响宿主
内存泄漏 定期检测循环引用(MLeaksFinder / Instruments Leaks)
监控 建立内存水位报警机制,超过阈值主动释放缓存

9. Watchdog 超时(0x8badf00d)

Q: 什么是 Watchdog 机制?如何避免和排查 Watchdog Crash?

Watchdog 机制

系统有一个看门狗线程监控主线程响应能力。如果主线程在规定时间内未完成特定回调,系统直接终止进程。

超时时限

场景 超时时间
启动(application:didFinishLaunchingWithOptions: 约 20 秒
前台无响应 约 10 秒(可能因系统版本和设备而异)
进入后台(applicationDidEnterBackground: 约 5-10 秒
挂起到前台恢复 约 10 秒

终止码

Crash 日志中的 Exception Code: 0x8badf00d(读作 "ate bad food")。

排查方法

  1. 查看 Crash 日志中主线程堆栈:卡在哪一步
  2. 常见原因
    • 启动阶段同步读取大文件
    • 主线程发起同步网络请求
    • 主线程等待锁(被子线程持有且长时间未释放)
    • 主线程执行大量计算(如 JSON 解析超大文件)
    • 数据库操作在主线程同步执行

防治方案

objectivec 复制代码
// 错误:主线程同步网络请求
NSData *data = [NSData dataWithContentsOfURL:url];

// 正确:异步
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSData *data = [NSData dataWithContentsOfURL:url];
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateUI:data];
    });
});
  • 启动阶段精简同步操作,能延迟的延迟,能异步的异步
  • 主线程只做 UI 操作
  • 大量 I/O、计算放到子线程
  • 监控主线程卡顿(见第 19 题)

10. 后台任务被系统杀死

Q: App 进入后台后可能被系统杀死的原因有哪些?如何申请额外的后台执行时间?

被杀原因

  1. 内存压力:后台 App 优先被 Jetsam 终止
  2. 后台任务超时beginBackgroundTaskWithExpirationHandler: 的任务未在限定时间内调用 endBackgroundTask:
  3. CPU 使用过高:后台持续占用大量 CPU 资源
  4. 违规后台行为:未声明后台模式却尝试执行后台操作

后台任务正确用法

objectivec 复制代码
- (void)applicationDidEnterBackground:(UIApplication *)application {
    __block UIBackgroundTaskIdentifier taskID = UIBackgroundTaskInvalid;
    taskID = [application beginBackgroundTaskWithName:@"SaveData"
                                   expirationHandler:^{
        // 必须在 handler 中结束任务,否则系统直接杀进程
        [application endBackgroundTask:taskID];
        taskID = UIBackgroundTaskInvalid;
    }];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saveDataToDisk];
        // 任务完成后立即结束
        [application endBackgroundTask:taskID];
        taskID = UIBackgroundTaskInvalid;
    });
}

常见错误

  • 忘记调用 endBackgroundTask: → 超时后系统杀进程并标记为 Watchdog 异常
  • expirationHandler 中没有结束任务
  • 申请了后台任务但实际已经完成了操作,未及时结束

11. Method Swizzling 在稳定性防护中的应用

Q: Method Swizzling 的原理是什么?在稳定性防护中使用有哪些坑和最佳实践?

原理

OC 方法调用通过 objc_msgSend 查找 IMP(方法实现),Swizzling 就是交换两个方法的 IMP 指针。

复制代码
交换前:
SEL_A → IMP_A
SEL_B → IMP_B

交换后:
SEL_A → IMP_B
SEL_B → IMP_A

标准实现

objectivec 复制代码
void swizzle(Class class, SEL originalSEL, SEL swizzledSEL) {
    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
    
    // 先尝试添加(处理原方法来自父类的情况)
    BOOL didAdd = class_addMethod(class,
                                   originalSEL,
                                   method_getImplementation(swizzledMethod),
                                   method_getTypeEncoding(swizzledMethod));
    if (didAdd) {
        class_replaceMethod(class,
                           swizzledSEL,
                           method_getImplementation(originalMethod),
                           method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

为什么要先 class_addMethod

如果子类没有 override 父类方法,直接 method_exchangeImplementations 会修改父类的方法列表,影响所有子类。先 class_addMethod 确保只在当前类上操作。

最佳实践

  1. +load 中执行,配合 dispatch_once+load 在类加载时调用,时机最早最安全;dispatch_once 确保只执行一次。
  2. Swizzled 方法中调用"自己":看起来像递归但实际是调用原始实现(因为 IMP 已交换)。
  3. 不要在 +initialize 中 Swizzle+initialize 可能被子类继承调用多次。
  4. 注意 Swizzle 顺序 :多个 Category 都 Swizzle 同一个方法时,+load 调用顺序取决于编译顺序。
  5. 线上慎用 :提供开关机制,可以远程关闭防护(例如 HSSafeKit 中的 hs_openSafeKit)。

12. 符号化与 Crash 日志分析

Q: 拿到一份 Crash 日志后,如何分析和符号化?

Crash 日志结构

yaml 复制代码
Incident Identifier: xxxxxx
Hardware Model:      iPhone12,1
Process:             MyApp [1234]
Exception Type:      EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype:   KERN_INVALID_ADDRESS at 0x0000000000000010

Thread 0 Crashed:
0   libobjc.A.dylib    0x1a2b3c4d5  objc_msgSend + 32
1   MyApp              0x1000abcde  0x100000000 + 703710
2   UIKitCore          0x1b2c3d4e5  -[UIViewController viewDidLoad] + 100

符号化步骤

  1. 获取 dSYM 文件:Xcode Archive 时自动生成,UUID 必须和二进制匹配。

  2. 验证 UUID 匹配

bash 复制代码
# 查看 dSYM UUID
dwarfdump --uuid MyApp.app.dSYM
# 查看二进制 UUID
dwarfdump --uuid MyApp.app/MyApp
  1. 使用 atos 符号化
bash 复制代码
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x1000abcde
# 输出: -[MyViewController handleTap:] (MyViewController.m:42)
  1. 使用 symbolicatecrash
bash 复制代码
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
symbolicatecrash MyApp.crash MyApp.app.dSYM > symbolicated.crash

关键信息分析

  • Exception Type:确定 Crash 类型
  • Exception Code0x8badf00d = Watchdog,0xd00d2bad = OOM
  • Crashed Thread 的堆栈:定位 Crash 发生的代码位置
  • Last Exception Backtrace:OC 异常的堆栈(NSException)
  • Binary Images:用于计算符号偏移

13. APM 与 Crash 监控体系搭建

Q: 如何设计一个完整的 Crash 监控体系?

三层捕获架构

yaml 复制代码
┌──────────────────────────────────────┐
│  Layer 1: OC 异常捕获                │
│  NSSetUncaughtExceptionHandler       │
├──────────────────────────────────────┤
│  Layer 2: Mach 异常捕获              │
│  task_set_exception_ports            │
├──────────────────────────────────────┤
│  Layer 3: Unix Signal 捕获           │
│  sigaction(SIGSEGV/SIGABRT/...)     │
└──────────────────────────────────────┘

捕获后的处理流程

scss 复制代码
Crash 发生
    ↓
1. 收集上下文信息(线程堆栈、寄存器、设备信息)
    ↓
2. 持久化到沙盒(写磁盘,不能用 OC/高级 API,用 C 的 write())
    ↓
3. 下次启动读取并上报到服务端
    ↓
4. 服务端符号化 + 聚合 + 报警

Crash 回调中的安全限制

Crash handler 运行在**异步信号不安全(Async-Signal-Unsafe)**环境中:

  • 不能使用 malloc(可能死锁,因为 malloc 内部有锁)
  • 不能使用 Objective-C 消息发送(objc_msgSend 可能死锁)
  • 不能使用 NSLogNSString 等高级 API
  • 应使用 write() 直接写文件
  • 预分配好写入 buffer

开源框架对比

框架 Mach 异常 Signal OC 异常 C++ 异常 符号化
PLCrashReporter 本地
KSCrash 本地+服务端
Bugly 服务端
Firebase Crashlytics 服务端

14. 多线程 Crash

Q: 多线程场景下有哪些常见 Crash?如何防护?

常见场景

1. 容器的非线程安全读写

objectivec 复制代码
// 线程 A:写
[self.mutableArray addObject:obj];

// 线程 B:读(或同时写)
NSLog(@"%@", self.mutableArray[0]);

// 可能触发 EXC_BAD_ACCESS

2. 属性的非原子访问

nonatomic 属性在多线程读写时可能读到"半成品"指针(写入时只完成了部分字节的赋值),导致野指针。

3. CoreData 多线程访问

NSManagedObjectContext 不是线程安全的,跨线程访问会导致不可预期的行为。

防护方案

方案 1:加锁

objectivec 复制代码
// 读写锁 pthread_rwlock(读多写少场景最优)
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);

// 读
pthread_rwlock_rdlock(&lock);
id obj = self.array[index];
pthread_rwlock_unlock(&lock);

// 写
pthread_rwlock_wrlock(&lock);
[self.mutableArray addObject:obj];
pthread_rwlock_unlock(&lock);

方案 2:GCD 并发队列 + barrier

objectivec 复制代码
dispatch_queue_t queue = dispatch_queue_create("com.app.safe",
                                                DISPATCH_QUEUE_CONCURRENT);

// 读
dispatch_sync(queue, ^{
    id obj = self.array[index];
});

// 写(barrier 确保写操作独占)
dispatch_barrier_async(queue, ^{
    [self.mutableArray addObject:obj];
});

方案 3:使用线程安全容器

  • @synchronized(简单但性能差)
  • 使用 NSCache(线程安全)替代 NSMutableDictionary 做缓存
  • CoreData 使用 performBlock: 确保在正确线程操作

Thread Sanitizer (TSan)

Xcode 内置的多线程问题检测工具,编译时插桩,可以检测:

  • 数据竞争(Data Race)
  • 线程间通过非同步方式共享数据

开启方式:Edit Scheme → Run → Diagnostics → Thread Sanitizer


15. Fishhook 与系统函数 Hook

Q: fishhook 的原理是什么?与 Method Swizzling 有何区别?在稳定性领域有什么应用?

Method Swizzling vs fishhook

特性 Method Swizzling fishhook
目标 OC 方法(SEL → IMP) C 函数(系统库)
原理 交换方法列表中的 IMP 修改 __DATA 段中的符号指针
范围 OC 对象方法/类方法 动态链接的 C 函数(如 malloc、objc_msgSend)
限制 只能 Hook OC 方法 只能 Hook 通过 dyld 动态绑定的函数

fishhook 原理

Mach-O 二进制使用 PIC(位置无关代码)调用外部函数:

markdown 复制代码
代码段(__TEXT,只读)
    call → 桩函数(stub)
              ↓
间接符号指针表(__DATA.__la_symbol_ptr,可写)
    存储实际函数地址
              ↓
dyld 在运行时绑定真实地址

fishhook 的做法:遍历间接符号表,找到目标符号,将其指针替换为自定义函数。

稳定性应用

1. Hook objc_msgSend 做方法耗时监控

c 复制代码
// 记录每个 OC 方法的调用耗时,发现卡顿方法
rebind_symbols((struct rebinding[1]){
    {"objc_msgSend", my_objc_msgSend, (void *)&orig_objc_msgSend}
}, 1);

2. Hook malloc/free 做内存分配监控

追踪大内存分配,帮助定位 OOM 问题。

3. Hook NSLog 减少线上日志开销

c 复制代码
static void (*orig_NSLog)(NSString *format, ...);
void my_NSLog(NSString *format, ...) {
    // 线上环境直接跳过 NSLog,减少性能开销
    #ifdef DEBUG
    va_list args;
    va_start(args, format);
    orig_NSLog(format, args);
    va_end(args);
    #endif
}

16. 线上 Crash 率治理实践

Q: 如何系统性地降低线上 Crash 率?

Crash 率指标

erlang 复制代码
Crash Rate = Crash UV / DAU × 100%

行业参考:
- 优秀:< 0.1%(千分之一)
- 合格:< 0.3%
- 需治理:> 0.5%

治理策略分层

第一层:防护兜底(短期见效)

防护类型 手段
Unrecognized Selector Hook forwardingTargetForSelector:
容器越界/nil Hook 容器类方法
KVO KVO 代理层
NSTimer 弱引用 target 防循环引用
NSNotification dealloc 自动 removeObserver

第二层:根因修复(中期)

  1. Top Crash 排序:按影响用户数排序,优先修复 Top 10
  2. 分版本分析:某版本新增的 Crash 优先处理
  3. 分设备/系统版本:特定设备或系统的 Crash 单独适配
  4. 堆栈聚合:相同 Crash 合并,避免重复分析

第三层:预防机制(长期)

  • CI/CD 集成静态分析(Clang Static Analyzer / Infer)
  • Code Review 重点关注多线程、内存管理
  • 灰度发布 + Crash 率实时监控
  • A/B 实验关联 Crash 率指标
  • 单元测试覆盖边界条件

17. 启动阶段 Crash 的特殊处理

Q: 如果 App 在启动阶段反复 Crash,如何处理?

难点

  • 启动阶段的 Crash 用户无法操作,导致 App 完全不可用
  • Crash 日志可能来不及上报
  • 如果是配置/数据导致的 Crash,每次启动都会重复

连续启动 Crash 保护方案

objectivec 复制代码
// 启动时记录
NSInteger crashCount = [[NSUserDefaults standardUserDefaults] 
                         integerForKey:@"launch_crash_count"];

// 标记启动中
[[NSUserDefaults standardUserDefaults] setInteger:crashCount + 1
                                           forKey:@"launch_crash_count"];
[[NSUserDefaults standardUserDefaults] synchronize];

if (crashCount >= 3) {
    // 连续 3 次启动 crash,执行修复策略
    [self performRecovery];
}

// 启动成功后(如 didBecomeActive 或首页展示后)重置计数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC),
               dispatch_get_main_queue(), ^{
    [[NSUserDefaults standardUserDefaults] setInteger:0
                                               forKey:@"launch_crash_count"];
});

修复策略

  1. 清除缓存数据:可能是脏数据导致
  2. 清除 UserDefaults:可能是错误配置导致
  3. 重置数据库:可能是数据库损坏
  4. 回退到安全模式:使用最小化功能启动
  5. 关闭防护框架 (如 HSSafeKit):防护框架本身可能导致启动 Crash(HSSafeKit 中的 hs_openSafeKit 开关设计即为此目的)

18. 堆栈回溯原理

Q: iOS Crash 时如何获取线程堆栈?有哪些回溯方式?

三种堆栈回溯方式

1. Frame Pointer 回溯(FP-Based)

ARM64 架构下,x29(FP)指向当前栈帧,栈帧中保存了上一帧的 FP 和返回地址(LR):

scss 复制代码
┌─────────────────┐  高地址
│  上层 FP(x29)  │ ← 当前 FP 指向这里
├─────────────────┤
│  返回地址 (LR)   │
├─────────────────┤
│  局部变量        │
└─────────────────┘  低地址

遍历链表即可回溯整个调用栈。性能高,但需要编译时不优化掉 Frame Pointer(-fno-omit-frame-pointer)。

2. DWARF Unwind

使用 DWARF 调试信息中的 .eh_frame / .debug_frame 段进行回溯。信息更准确,但性能开销大。

3. Compact Unwind

Apple 平台特有的压缩格式,__TEXT.__unwind_info 段存储,比 DWARF 更紧凑高效。

实际获取堆栈

objectivec 复制代码
// OC 层(简单但信息有限)
NSArray *symbols = [NSThread callStackSymbols];
NSArray *addresses = [NSThread callStackReturnAddresses];

// C 层(更底层)
#include <execinfo.h>
void *callstack[128];
int frames = backtrace(callstack, 128);
char **symbols = backtrace_symbols(callstack, frames);

// 获取所有线程堆栈(Mach API)
thread_act_array_t threads;
mach_msg_type_number_t threadCount;
task_threads(mach_task_self(), &threads, &threadCount);

for (int i = 0; i < threadCount; i++) {
    _STRUCT_MCONTEXT machineContext;
    mach_msg_type_number_t stateCount = ARM_THREAD_STATE64_COUNT;
    thread_get_state(threads[i], ARM_THREAD_STATE64,
                     (thread_state_t)&machineContext.__ss, &stateCount);
    // 从 machineContext 中获取 PC、LR、FP 进行回溯
}

19. 卡顿监控与治理

Q: 如何监控主线程卡顿?常用方案有哪些?

方案 1:RunLoop Observer 监控

原理:监听 RunLoop 的状态切换,如果长时间停留在某个状态(如 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting),说明主线程被阻塞。

objectivec 复制代码
static void runLoopObserverCallback(CFRunLoopObserverRef observer,
                                     CFRunLoopActivity activity,
                                     void *info) {
    // 记录 RunLoop 状态变化时间
    switch (activity) {
        case kCFRunLoopBeforeSources:
            // 开始处理事件
            break;
        case kCFRunLoopAfterWaiting:
            // 唤醒后开始执行
            break;
    }
}

// 子线程定时检查状态是否长时间未变化
// 超过阈值(如 200ms)则记录堆栈

方案 2:子线程 Ping 主线程

objectivec 复制代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    while (YES) {
        __block BOOL responded = NO;
        dispatch_async(dispatch_get_main_queue(), ^{
            responded = YES;
        });
        
        [NSThread sleepForTimeInterval:0.2]; // 等待 200ms
        
        if (!responded) {
            // 主线程卡顿,抓取主线程堆栈
            [self captureMainThreadStack];
        }
    }
});

方案 3:Hook objc_msgSend(方法级耗时统计)

通过 fishhook 替换 objc_msgSend,在前后记录时间戳,精确到每个方法的耗时。适合线下 profiling,线上开销过大。

卡顿治理方向

问题 优化方案
主线程 I/O 移到子线程(网络、文件读写、数据库)
主线程大量计算 拆分任务或移到子线程
过度布局计算 缓存 cell 高度,使用 estimatedRowHeight
离屏渲染 减少 cornerRadius + masksToBounds,使用预渲染
图片解码 子线程预解码(SDWebImage 默认处理)
锁竞争 减小锁粒度,避免主线程等待子线程锁

20. 磁盘与数据库稳定性

Q: 磁盘写入和数据库操作在稳定性方面需要注意什么?

磁盘写入

1. 沙盒空间不足

iOS 设备存储满时写入失败,需要:

  • 写入前检查可用空间
  • 捕获写入错误并上报
  • 清理过期缓存释放空间
objectivec 复制代码
NSError *error;
NSDictionary *attrs = [[NSFileManager defaultManager]
                        attributesOfFileSystemForPath:NSHomeDirectory()
                        error:&error];
uint64_t freeSpace = [attrs[NSFileSystemFreeSize] unsignedLongLongValue];

2. 原子写入

NSData writeToFile:atomically:YES 先写临时文件再 rename,确保写入的原子性。如果写入过程中 Crash,原文件不会损坏。

SQLite / CoreData 稳定性

1. 数据库损坏

原因:

  • 写入过程中异常中断(crash / 断电)
  • 多进程/多线程并发写入冲突
  • 磁盘空间不足

防护:

sql 复制代码
-- 开启 WAL 模式,提高并发性能和安全性
PRAGMA journal_mode=WAL;

-- 定期完整性检查
PRAGMA integrity_check;

2. 损坏后恢复策略

objectivec 复制代码
if (![self openDatabase]) {
    // 数据库损坏
    // 方案 1:删除重建
    [[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil];
    [self openDatabase];
    
    // 方案 2:从备份恢复
    [self restoreFromBackup];
    
    // 方案 3:尝试 dump 并重建
    // 使用 ".dump" 命令导出可恢复数据
}

3. CoreData 的轻量级迁移

模型升级时使用轻量级迁移,失败时降级处理:

objectivec 复制代码
NSDictionary *options = @{
    NSMigratePersistentStoresAutomaticallyOption: @YES,
    NSInferMappingModelAutomaticallyOption: @YES
};

NSError *error;
if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
                               configuration:nil
                                         URL:storeURL
                                     options:options
                                       error:&error]) {
    // 迁移失败,删除旧数据库重建
    [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
    [coordinator addPersistentStoreWithType:NSSQLiteStoreType
                             configuration:nil
                                       URL:storeURL
                                   options:options
                                     error:nil];
}

附录:高频追问

Q: @try-@catch 能捕获所有 Crash 吗?

不能。@try-@catch 只能捕获 OC 异常(NSException),无法捕获:

  • EXC_BAD_ACCESS(野指针、内存越界)
  • SIGABRT(非 NSException 触发的)
  • SIGSEGV
  • 其他系统级异常

而且 @try-@catch 在 ARC 下有额外开销(需要维护异常处理的 cleanup 表)。

Q: App 的 Crash 日志存储在哪里?

  • 设备上:设置 → 隐私 → 分析与改进 → 分析数据
  • Xcode:Window → Devices and Simulators → View Device Logs
  • 通过 MetricKit 框架:iOS 13+ 可以在 App 内接收系统级 Crash 和性能数据

Q: +load+initialize 的区别是什么?为什么 Swizzle 要放在 +load

特性 +load +initialize
调用时机 dyld 加载类时(main 之前) 类第一次收到消息时
调用次数 每个类/分类各调用一次 可能多次(子类未实现会调用父类的)
线程安全 系统加锁保证线程安全 系统加锁保证线程安全
是否继承 不继承 继承
影响启动 过多 +load 会拖慢启动 惰性调用不影响启动

Swizzle 放在 +load 的原因:

  1. 时机最早,确保方法被调用前已经完成交换
  2. 不会被子类继承导致多次调用
  3. +initialize 有被多次调用的风险,可能导致重复 Swizzle(偶数次交换等于没交换)

Q: Swift 中的 Crash 有什么不同?

  1. 强制解包 nillet value: String = optional!,Optional 为 nil 时触发 SIGTRAP
  2. 数组越界 :Swift 数组越界直接触发 SIGTRAP(不像 OC 可以 Hook)
  3. fatalError / preconditionFailure :直接 SIGTRAP
  4. as! 类型转换失败
  5. Swift Crash 更难做运行时防护(缺少 OC 的消息转发机制),更依赖编译期检查和代码质量

最后更新: 2026-04-16

相关推荐
陆枫Larry2 小时前
一次讲清楚 `Promise.finally()`:为什么“无论成功失败都要执行”该用它
前端
Momo__2 小时前
被低估的 HTML 原生表单元素:dialog、datalist、meter、progress
前端
莹宝思密达2 小时前
【AI】chrome-dev-tools-mcp
前端·ai
用户69371750013842 小时前
2026 Android 开发,现在还能入行吗?
android·前端·ai编程
SuperEugene2 小时前
Vue3 配置驱动弹窗:JSON配置弹窗内容/按钮,避免重复开发弹窗|配置驱动开发实战篇
前端·javascript·vue.js·前端框架·json
WayneYang2 小时前
前端 JavaScript 核心知识点 + 高频踩坑 + 大厂面试题全汇总(开发 / 面试必备)
前端·javascript
小贵子的博客2 小时前
基于Vue3 和 Ant Design Vue实现Modal弹窗拖拽组件
前端·javascript·vue.js
小李子呢02113 小时前
前端八股CSS---CSS选择器和优先级
前端·css
阿凤213 小时前
uniapp如何修改下载文件位置
开发语言·前端·javascript