一、前言:为什么你的线上问题永远定位不准?
做 iOS 开发久了,几乎所有人都遇到过这些经典线上问题:
- 测试环境百分百稳定,上线小规模用户偶现闪退,无法复现
- Bugly/友盟 只报堆栈,不知道用户操作路径、上下文现场
- 后台崩溃、OOM 内存溢出、卡死 ANR,无堆栈、无日志
- 修复完问题过段时间再次复发,没有复盘、没有规范沉淀
很多团队的稳定性体系只停留在「接入 Bugly 看崩溃报表」,这是 被动式维稳。真正企业级大型项目需要一套闭环体系:
崩溃全覆盖捕获 → 精细化日志留存 → 结构化问题分析 → 标准化复盘沉淀 → 前置预防兜底
本文手把手搭建 iOS 完整线上稳定性体系,包含可直接上线的崩溃捕获代码、分级日志框架、日志脱敏加密、真实线上案例分析、标准化复盘模板,全部落地可用。
二、iOS 线上崩溃的完整分类(90% 崩溃都逃不出这 5 类)
想要做好监测,首先要知道「我们要捕获什么」,iOS 线上崩溃分为五大类,覆盖所有闪退场景:
1. OC 异常崩溃(NSException)
常见场景:数组越界、字典插空、参数非法、UI 主线程违规、KVO 重复移除。
特点:有完整堆栈,最容易定位。
2. Signal 信号崩溃(Mach-O 底层崩溃)
常见场景:野指针、内存踩踏、重复 free、多线程读写冲突。
常见信号:SIGSEGV、SIGABRT、SIGBUS、SIGILL。
特点:第三方平台经常丢失堆栈,必须自研捕获兜底。
3. Swift 运行时崩溃
常见场景:隐式解包 nil、数组越界、类型强制转换失败、try 未捕获错误。
4. 卡死 / ANR WatchDog 崩溃
主线程卡顿超时被系统杀死,无崩溃堆栈、第三方平台经常不报,是线上隐性闪退重灾区。
5. 内存类崩溃(OOM / Jetsam)
内存峰值过高、后台占用内存超标被系统 Jetsam 机制杀进程,无任何崩溃日志,只能靠性能日志分析。
核心结论:只依赖 Bugly 只能捕获 60% 常规崩溃,信号崩溃、卡死、OOM 大量丢失,必须「第三方监控 + 自研底层捕获 + 本地日志兜底」三位一体。
三、搭建全覆盖崩溃捕获体系(可直接上线代码)
我们搭建一套自研崩溃捕获框架,兜底所有类型崩溃,弥补第三方平台缺失。
1. 捕获 OC 异常崩溃(NSException 全局捕获)
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface CrashMonitor : NSObject
+ (void)startMonitor;
@end
static void globalUncaughtExceptionHandler(NSException *exception) {
// 1. 获取崩溃核心信息
NSString *name = exception.name;
NSString *reason = exception.reason;
NSArray *stackSymbols = exception.callStackSymbols;
// 2. 组装崩溃现场:设备、版本、操作栈、时间
NSMutableDictionary *crashInfo = [NSMutableDictionary dictionary];
crashInfo[@"crash_name"] = name;
crashInfo[@"crash_reason"] = reason;
crashInfo[@"crash_stack"] = [stackSymbols componentsJoinedByString:@"\n"];
crashInfo[@"version"] = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"];
crashInfo[@"device"] = [UIDevice currentDevice].model;
crashInfo[@"system"] = [UIDevice currentDevice].systemVersion;
// 3. 本地加密写入日志 + 延迟上报
[LogManager saveCrashLog:crashInfo];
// 4. 留给第三方SDK继续上报
}
@implementation CrashMonitor
+ (void)startMonitor {
// 注册全局异常捕获
NSSetUncaughtExceptionHandler(&globalUncaughtExceptionHandler);
}
@end
2. 捕获 Signal 信号崩溃(野指针、内存踩踏兜底)
这是第三方平台最容易丢失的崩溃类型,也是大型项目疑难闪退的核心来源。
#include <signal.h>
// 监听常见崩溃信号
static int crashSignals[] = {
SIGSEGV,
SIGABRT,
SIGBUS,
SIGILL,
SIGFPE
};
static void signalCrashHandler(int sig) {
// 获取线程堆栈、信号类型
NSString *signalMsg = [NSString stringWithFormat:@"Signal Crash: %d", sig];
NSArray *stack = [NSThread callStackSymbols];
NSMutableDictionary *crashInfo = [NSMutableDictionary dictionary];
crashInfo[@"crash_type"] = @"Signal";
crashInfo[@"crash_reason"] = signalMsg;
crashInfo[@"crash_stack"] = [stack componentsJoinedByString:@"\n"];
[LogManager saveCrashLog:crashInfo];
// 退出进程,避免继续异常
exit(EXIT_FAILURE);
}
// 注册信号监听
- (void)registerSignalMonitor {
int count = sizeof(crashSignals) / sizeof(int);
for (int i = 0; i < count; i++) {
signal(crashSignals[i], signalCrashHandler);
}
}
3. 主线程卡死 ANR 监测(解决无堆栈闪退)
通过主线程 RunLoop 心跳检测,识别卡死、ANR 问题,弥补系统 WatchDog 无日志缺陷。
// 主线程卡顿监测 Swift 版
class ANRMonitor {
static let shared = ANRMonitor()
private var isRunning = false
private let checkInterval: TimeInterval = 0.8
func start() {
guard !isRunning else { return }
isRunning = true
DispatchQueue.global().async { [weak self] in
while self?.isRunning ?? false {
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.main.async {
semaphore.signal()
}
// 超时说明主线程卡死
let result = semaphore.wait(timeout: .now() + self.checkInterval)
if result == .timedOut {
// 记录卡顿堆栈
let stack = Thread.callStackSymbols
LogManager.saveANRLog(stack: stack)
}
}
}
}
}
4. 监测体系组合方案(企业级标准)
Bugly/Firebase + 自研底层捕获 + ANR 卡顿监测
- 第三方:负责大数据量统计、崩溃聚合、版本环比、告警推送
- 自研捕获:兜底信号崩溃、卡死崩溃、丢失堆栈问题
- 本地日志:留存崩溃前后上下文,解决「有堆栈无现场」问题
四、企业级分级日志体系(可落地、脱敏、加密、可上报)
崩溃只是结果,日志才是根因。没有上下文日志,90% 偶现崩溃无法定位。
1. 日志分级规范(统一团队标准)
- DEBUG:开发调试日志,打包自动关闭
- INFO:正常业务流程(页面打开、接口请求、缓存读写)
- WARN:非致命异常(缓存失效、参数缺失、重试触发)
- ERROR:业务错误(接口失败、数据解析失败、文件读写失败)
- FATAL:崩溃、严重异常(配合崩溃捕获)
2. 日志分类维度(排查问题极速定位)
- 业务日志:页面生命周期、用户点击、业务流程
- 网络日志:请求地址、参数、响应码、耗时
- 存储日志:沙盒读写、FMDB/Realm 操作、加密解密结果
- 性能日志:帧率、内存、CPU、卡顿点
- 崩溃日志:异常信息、信号堆栈、卡死记录
3. 核心能力:日志脱敏 + 加密存储(合规必备)
绝对禁止明文日志记录:手机号、Token、身份证、地址,会引发隐私合规风险。
// 日志脱敏工具
class LogSensitiveFilter {
static func filterSensitive(_ content: String) -> String {
var result = content
// 手机号脱敏 138****1234
result = result.replacingOccurrences(of: "(1[0-9]{2})[0-9]{4}([0-9]{4})", with: "$1****$2", options: .regularExpression)
// Token 整体脱敏
result = result.replacingOccurrences(of: "token=\w+", with: "token=***", options: .regularExpression)
// 身份证脱敏
result = result.replacingOccurrences(of: "([0-9]{6})[0-9]{8}([0-9]{4})", with: "$1****$2", options: .regularExpression)
return result
}
}
4. 日志沙盒加密存储 + 自动清理
日志存放路径规范:Library/Application Support/Log/ (不备份、持久化、不被系统清理)
能力:按天分割、7天自动过期、AES加密、崩溃前完整留存
五、线上问题分析实战(3个真实线上案例)
案例1:FMDB 多队列导致 SQLITE_BUSY 偶现崩溃
现象:用户频繁进出聊天页,偶现闪退,Bugly 堆栈模糊,无法稳定复现。
日志现场:
- 子线程 A 开启批量写入事务未关闭
- 主线程 B 立即执行查询操作
- 数据库文件锁冲突 SQLITE_BUSY,超时崩溃
根因:项目多处创建 FMDatabaseQueue,多队列竞争同一数据库文件锁。
修复方案:全局单例队列统一调度,所有读写收敛至全局 Queue。
案例2:沙盒 tmp 目录存储关键数据导致随机丢失、闪退
现象:部分用户离线数据偶现清空,偶发文件读取空指针崩溃。
根因:开发将离线解密文件放在 tmp 目录,系统后台回收清理文件。
修复:核心数据迁移至 Application Support,临时文件仅用作中转。
案例3:主线程大量文件解密导致 ANR 卡死、系统闪退
现象:页面滑动卡顿,部分用户直接 WatchDog 杀进程,无崩溃堆栈。
日志定位:主线程执行 AES 大文件解密,阻塞 RunLoop 超过 1.2s。
修复:所有文件加解密、IO 操作全部迁移至子线程。
六、标准化问题复盘体系(团队可直接套用)
没有复盘的 Bug,一定会重蹈覆辙。统一 四段式复盘模板。
1. 复盘标准四要素
- 现象:崩溃率、影响用户、触发场景、版本范围
- 根因:代码层级根本原因(禁止归因「系统问题、偶现问题」)
- 修复:临时修复方案、版本生效时间
- 规避:长期规范、代码检查、监控兜底、新增单测
2. 复盘沉淀落地机制
- 高频崩溃录入团队问题库
- 对应代码增加注释与告警日志
- CI 流水线增加高危代码扫描
- 新增专项测试用例,防止回归
七、线上稳定性兜底预防机制
1. 代码层自愈兜底
- 数组、字典、参数强制判空兜底
- 数据库事务失败自动回滚
- 文件读写异常捕获、损坏文件自动重建
- 网络失败重试、弱网/断网降级
2. 监控告警机制
- 版本崩溃率 > 0.5% 触发告警
- 新增崩溃类型立即推送
- 卡顿率、OOM 率、页面加载耗时实时监控
3. 灰度与回滚机制
小流量灰度发布,监控稳定性指标,异常立即热修复或版本回滚,杜绝大规模线上事故。
八、全文总结
-
单纯依赖第三方崩溃平台无法覆盖全部线上问题,必须自研底层捕获兜底信号崩溃、卡死、OOM 场景。
-
日志是定位疑难崩溃的核心,必须做到分级、分类、脱敏、加密、留存、可上报。
-
所有线上问题必须走标准化复盘闭环,从「修 Bug」升级为「根治问题、完善规范」。
-
真正的线上稳定性 = 全量捕获 + 精细化日志 + 精准分析 + 复盘沉淀 + 前置预防。