# 移动端的“安全气垫” > 安全气垫是端上一个老生常谈的话题,技术方案已经是好多年前的产物。唯一的问题是不同公司对于安全气垫在拦截到异常的时候,“做与不做”策略的判断不同。比如数组索引越界的时候要返回一个错误位置的值吗?会不会产生业务异常?要一刀切吗?那么本文就尝试回答下这个问题 ## 一、一个经典的场景 Weex 源码中,设计了一个 `WXThreadSafeMutableDictionary`。在字典操作的地方使用锁: ```Objective-C - (void)setObject:(id)anObject forKey:(id)aKey { id originalObject = nil; // make sure that object is not released in lock @try { pthread_mutex_lock(&_safeThreadDictionaryMutex); originalObject = [_dict objectForKey:aKey]; [_dict setObject:anObject forKey:aKey]; } @finally { pthread_mutex_unlock(&_safeThreadDictionaryMutex); } originalObject = nil; } ``` 这么写的价值:**解锁逻辑「绝对执行」,彻底避免死锁** 这是 `@try-finally` 最核心的价值 ——无论 try 块内发生什么(正常执行、提前 return、抛异常),finally 块的解锁逻辑一定会执行 对比无 try-finally 的写法 ```Objective-C // Bad: 若setObject抛异常,unlock不会执行→死锁 pthread_mutex_lock(&_mutex); [_dict setObject:anObject forKey:aKey]; pthread_mutex_unlock(&_mutex); ``` 问题:`[_dict setObject:anObject forKey:aKey]` 可能抛异常(比如 aKey = nil 时会触发 NSInvalidArgumentException),若没有 finally,锁会被永久持有→其他线程调用 lock 时死锁,整个字典无法再操作。 设计优点: - `@try-finallly`:即使 try 内逻辑出错,finally 也会执行 pthread_mutex_unlock,保证锁最终释放,这是**线程安全的「兜底保障」** - 注意,不是 `try...catch...finally`: 如果加了 catch 逻辑,则字典的 key 为 nil 产生的崩溃也会被捕获掉,这属于不符合预期的行为。因为 key 为 nil 产生的原因太多了,可能是业务代码异常,也可能是数据异常,也可能是逻辑错误,如果一刀切直接用 `try...catch...finally` 捕获了异常,但是没有配置异常的收集、上报、处理逻辑,属于边界不清晰,本质是为了解决加解锁不匹配而可能带来的线程安全问题,却"多管闲事",把字典 key 为 nil 本该向上跑的异常而卡住了。 这是一个经典的策略问题,端上的异常发生时,安全气垫的“做与不做”问题 聊聊类似网易的大白解决方案或者业界其他公司中,安全气垫虽然保证了代码不 crash,影响用户体验,但是比如数组本该越界,现在却不越界: 1. 唯一能做的就是返回一个错误的值,比如数组长度为3,访问4,现在不 crash,返回了 0 的值,那是不是产生了业务异常?比如商品价格 2. 不 crash,也不返回错误位置的值,类似给一个回调,告诉业务方出现了异常,可以做一些业务层面的提醒或者配置(比如开发阶段商品卡片的价格 Label 显示:商品价格获取错误,数组越界),同时产生的异常案发现场信息和其他的一些数据会上报,用于 APM 平台去分析和定位。 但这也产生一个问题,类似数组越界的场景,可能10000次里面9999次都正常,只有1次异常,业务开发为了这万分之一出现的异常,还需要写一些异常处理的逻辑(比如商品卡片展示价格获取错误,数组越界)。那字典的 key 为 nil 呢?除法的分母为0呢?诸如此类,类似乐观锁和悲观锁的场景 ## 二、核心原则 要解决「安全气垫防崩溃但引发隐性业务异常」「低概率异常导致业务开发冗余逻辑」的核心矛盾,业界的优雅方案核心思路是:「环境差异化策略」+「分层兜底 + 语义化默认值」+「可观测驱动的轻量处理」,既避免线上 Crash,又最小化业务侵入,同时保证问题可被发现和修复。 **开发阶段让问题 “炸出来”,生产阶段让问题 “软落地”**。从源头减少线上低概率异常的发生,业务开发无需为 “万分之一” 的异常写冗余逻辑 | 环境 | 核心目标 | 策略 | | ----------- | ---------------------------------- | ----------------------------------- | | 开发 / 测试 | 提前暴露问题,杜绝上线 | 「零容忍」:直接 Crash + 详细上下文 | | 生产 | 避免 Crash + 可观测 + 最小业务影响 | 「软兜底」:语义化默认值 + 全量上报 | ## 三、多个方案 ### 方案1:环境差异化 + 开发强感知(网易大白/腾讯 Bugly 核心思路) 对 NSArray、NSDictionary、NSNumber 等基础类做运行时 Hook,区分环境处理异常: NSArray+DWSafeHook.m ```Objective-C // 核心逻辑: // 1. Method Swizzling Hook数组核心读写方法 // 2. Debug环境:越界直接Crash+详细上下文,强制研发修复 // 3. Release环境:拦截崩溃+上报异常+返回语义化空值,避免用户感知 // #import "NSArray+DWSafeHook.h" #import #import "DWEnvironmentUtils.h" // 环境判断工具类(自研) #import "DWCrashReporter.h" // 崩溃上报工具类(自研) @implementation NSArray (DWSafeHook) #pragma mark - 对外入口:初始化Hook + (void)dw_setupSafeHook { // 防止重复Hook static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Hook 只读数组核心方法 [self dw_swizzleInstanceMethod:@selector(objectAtIndex:) withNewMethod:@selector(dw_safe_objectAtIndex:)]; [self dw_swizzleInstanceMethod:@selector(objectAtIndexedSubscript:) withNewMethod:@selector(dw_safe_objectAtIndexedSubscript:)]; // Hook 可变数组核心方法(写操作) [NSMutableArray dw_swizzleInstanceMethod:@selector(addObject:) withNewMethod:@selector(dw_safe_addObject:)]; [NSMutableArray dw_swizzleInstanceMethod:@selector(insertObject:atIndex:) withNewMethod:@selector(dw_safe_insertObject:atIndex:)]; [NSMutableArray dw_swizzleInstanceMethod:@selector(removeObjectAtIndex:) withNewMethod:@selector(dw_safe_removeObjectAtIndex:)]; }); } #pragma mark - 私有工具:Method Swizzling封装 /// 通用Swizzling方法(避免重复代码) /// @param originalSEL 原方法SEL /// @param newSEL 替换后的方法SEL + (void)dw_swizzleInstanceMethod:(SEL)originalSEL withNewMethod:(SEL)newSEL { Class cls = [self class]; // 获取原方法和新方法 Method originalMethod = class_getInstanceMethod(cls, originalSEL); Method newMethod = class_getInstanceMethod(cls, newSEL); if (!originalMethod || !newMethod) { NSLog(@"[DWCrashGuard] Swizzling失败:方法不存在 originalSEL: %@, class: %@", NSStringFromSelector(originalSEL), NSStringFromClass(cls)); return; } // 尝试添加新方法(防止原方法未实现) BOOL isAdded = class_addMethod(cls, originalSEL, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)); if (isAdded) { // 替换原方法实现 class_replaceMethod(cls, newSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 交换方法实现 method_exchangeImplementations(originalMethod, newMethod); } } #pragma mark - Hook实现:只读数组读操作(核心防越界) /// 拦截 [array objectAtIndex:] 越界 - (id)dw_safe_objectAtIndex:(NSUInteger)index { // 安全校验:索引越界判断 if (index >= self.count) { [self dw_handleArrayOutOfBoundsWithIndex:index method:@"objectAtIndex:"]; return [NSNull null]; // Release环境返回语义化空值(而非0) } // 正常逻辑:调用原方法(Swizzle后,此处实际是原objectAtIndex:) return [self dw_safe_objectAtIndex:index]; } /// 拦截 array[index] 下标访问越界 - (id)dw_safe_objectAtIndexedSubscript:(NSUInteger)index { if (index >= self.count) { [self dw_handleArrayOutOfBoundsWithIndex:index method:@"objectAtIndexedSubscript:"]; return [NSNull null]; } return [self dw_safe_objectAtIndexedSubscript:index]; } #pragma mark - 私有工具:数组越界统一处理 /// 数组越界异常处理(区分环境) /// @param index 访问的索引 /// @param method 触发异常的方法名 - (void)dw_handleArrayOutOfBoundsWithIndex:(NSUInteger)index method:(NSString *)method { // 1. 构建异常上下文(用于上报/调试) NSDictionary *context = @{ @"crashType": @"NSArrayOutOfBounds", @"method": method, @"arrayCount": @(self.count), @"accessIndex": @(index), @"arrayDescription": [self description], // 数组内容(便于定位) @"callStack": [NSThread callStackSymbols], // 完整调用栈 @"timestamp": @([[NSDate date] timeIntervalSince1970]), @"deviceInfo": [DWEnvironmentUtils deviceInfo] // 设备型号/系统版本等 }; // 2. 区分环境处理 if ([DWEnvironmentUtils isDebugEnvironment]) { // Debug环境:直接Crash+详细日志,强制研发修复 NSString *errorMsg = [NSString stringWithFormat: @"【网易大白】NSArray越界崩溃!\n" @"方法:%@\n" @"数组长度:%lu,访问索引:%lu\n" @"数组内容:%@\n" @"调用栈:%@", method, self.count, index, self, [NSThread callStackSymbols]]; NSAssert(NO, @"%@", errorMsg); abort(); // 确保Crash(NSAssert在Release下失效) } else { // Release环境:拦截崩溃+上报APM平台 [DWCrashReporter reportCrashWithType:@"NSArrayOutOfBounds" context:context severity:DWCrashSeverityLow]; // 低优先级(万分之一概率) NSLog(@"[DWCrashGuard] 拦截NSArray越界:%@, count:%lu, index:%lu", method, self.count, index); } } @end #pragma mark - 可变数组Hook实现(写操作防护) @implementation NSMutableArray (DWSafeHook) /// 拦截 addObject:nil 崩溃 - (void)dw_safe_addObject:(id)anObject { if (anObject == nil) { // 构建异常上下文 NSDictionary *context = @{ @"crashType": @"NSMutableArrayAddNil", @"callStack": [NSThread callStackSymbols], @"timestamp": @([[NSDate date] timeIntervalSince1970]) }; if ([DWEnvironmentUtils isDebugEnvironment]) { // Debug环境:Crash+提示 NSAssert(NO, @"【网易大白】NSMutableArray添加nil对象!调用栈:%@", [NSThread callStackSymbols]); abort(); } else { // Release环境:拦截+上报,忽略nil添加 [DWCrashReporter reportCrashWithType:@"NSMutableArrayAddNil" context:context severity:DWCrashSeverityLow]; NSLog(@"[DWCrashGuard] 拦截NSMutableArray添加nil对象"); return; } } // 正常逻辑:调用原方法 [self dw_safe_addObject:anObject]; } /// 拦截 insertObject:atIndex: 越界/nil - (void)dw_safe_insertObject:(id)anObject atIndex:(NSUInteger)index { // 1. 校验nil if (anObject == nil) { [self dw_handleMutableArrayNilObjectWithMethod:@"insertObject:atIndex:"]; return; } // 2. 校验越界 if (index > self.count) { // insert允许index == count(追加) [self dw_handleArrayOutOfBoundsWithIndex:index method:@"insertObject:atIndex:"]; return; } // 正常逻辑 [self dw_safe_insertObject:anObject atIndex:index]; } /// 拦截 removeObjectAtIndex: 越界 - (void)dw_safe_removeObjectAtIndex:(NSUInteger)index { if (index >= self.count) { [self dw_handleArrayOutOfBoundsWithIndex:index method:@"removeObjectAtIndex:"]; return; } // 正常逻辑 [self dw_safe_removeObjectAtIndex:index]; } #pragma mark - 私有工具:可变数组nil处理 - (void)dw_handleMutableArrayNilObjectWithMethod:(NSString *)method { NSDictionary *context = @{ @"crashType": @"NSMutableArrayInsertNil", @"method": method, @"callStack": [NSThread callStackSymbols] }; if ([DWEnvironmentUtils isDebugEnvironment]) { NSAssert(NO, @"【网易大白】NSMutableArray插入nil对象!方法:%@,调用栈:%@", method, [NSThread callStackSymbols]); abort(); } else { [DWCrashReporter reportCrashWithType:@"NSMutableArrayInsertNil" context:context severity:DWCrashSeverityLow]; NSLog(@"[DWCrashGuard] 拦截NSMutableArray插入nil对象:%@", method); } } @end ``` 简化下,逻辑基本为: ```Objective-C // 伪代码:Hook NSArray的objectAtIndex: - (id)safe_objectAtIndex:(NSUInteger)index { if (index >= self.count) { // 开发环境:Crash + 打印完整的案发现场信息 + 调用栈 + 上下文(数组内容、访问索引、业务模块) #ifdef DEBUG NSAssert(NO, @"数组越界:数组长度%lu,访问索引%lu,调用栈:%@", self.count, index, [NSThread callStackSymbols]); abort(); #else // 生产环境:返回语义化空值(而非0)+ 上报APM [APMManager reportExceptionWithType:@"数组越界" context:@{@"arrayCount": @(self.count), @"index": @(index), @"callStack": [NSThread callStackSymbols]}]; return [NSNull null]; // 而非0,业务层可统一识别,比如价格展示为: '--' #endif } return [self original_objectAtIndex:index]; } ``` 核心优势: - 开发环境:不仅 Crash,还打印「业务数据上下文」(比如当前是商品详情页、数组是价格数组),开发一眼定位问题 - 生产环境:返回NSNull(而非无意义的 0),业务层只需做一次全局 UI 处理(比如所有 Label 展示时,判断值为NSNull则显示 “--”),无需为每个场景写逻辑 覆盖场景: - 数组越界:返回NSNull; - 字典 key 为 nil:setObject:forKey:时忽略 nil key 并上报,objectForKey:时返回NSNull; - 分母为 0:返回INFINITY(全局工具类判断isinf(),统一返回 “--”); - 字符串转数字失败:返回NSNull而非 0 ### 方案2:声明式全局兜底 + 业务按需关注(阿里/字节) 避免业务层 “零散处理异常”,而是**全局统一兜底 + 业务选择性注册关注的异常类型**。 - 全局层面:所有基础类异常返回 NSNull,UI 层统一处理 NSNull 为 “--”/“获取失败”; - 业务层面:仅对 “核心场景”(如商品价格、支付金额)注册异常回调,非核心场景无需处理: WXExceptionManager.h ```Objective-C #import NS_ASSUME_NONNULL_BEGIN /// 异常类型枚举(替代字符串,避免硬编码) typedef NS_ENUM(NSUInteger, WXExceptionType) { WXExceptionTypeArrayOutOfBounds, // 数组越界 WXExceptionTypeDictionaryNilKey, // 字典nil key WXExceptionTypeDivideByZero, // 分母为0 }; /// 异常上下文Key定义(统一常量,避免拼写错误) FOUNDATION_EXTERN NSString *const WXExceptionContextBizModuleKey; // 业务模块 FOUNDATION_EXTERN NSString *const WXExceptionContextArrayCountKey;// 数组长度 FOUNDATION_EXTERN NSString *const WXExceptionContextAccessIndexKey;// 访问索引 FOUNDATION_EXTERN NSString *const WXExceptionContextCallStackKey; // 调用栈 FOUNDATION_EXTERN NSString *const WXExceptionContextExtraKey; // 扩展信息 @interface WXExceptionManager : NSObject /// 单例(全局唯一) + (instancetype)sharedManager; /// 注册异常回调 /// @param type 异常类型 /// @param handler 回调(主线程执行,避免UI操作崩溃) - (void)registerCallbackForType:(WXExceptionType)type handler:(void(^)(NSDictionary *context))handler; /// 上报异常(内部Hook调用,业务层无需调用) /// @param type 异常类型 /// @param context 异常上下文 - (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context; @end NS_ASSUME_NONNULL_END ``` WXExceptionManager.m ```Objective-C #import "WXExceptionManager.h" #import // 上下文Key常量定义 NSString *const WXExceptionContextBizModuleKey = @"bizModule"; NSString *const WXExceptionContextArrayCountKey = @"arrayCount"; NSString *const WXExceptionContextAccessIndexKey = @"accessIndex"; NSString *const WXExceptionContextCallStackKey = @"callStack"; NSString *const WXExceptionContextExtraKey = @"extra"; @interface WXExceptionManager () /// 存储不同异常类型的回调(key: WXExceptionType的NSNumber,value: 回调数组) @property (nonatomic, strong) NSMutableDictionary *> *callbackDict; /// 线程安全锁 @property (nonatomic, assign) pthread_mutex_t mutex; @end @implementation WXExceptionManager + (instancetype)sharedManager { static WXExceptionManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (instancetype)init { self = [super init]; if (self) { _callbackDict = [NSMutableDictionary dictionary]; // 初始化线程锁 pthread_mutex_init(&_mutex, NULL); } return self; } /// 注册异常回调 - (void)registerCallbackForType:(WXExceptionType)type handler:(void (^)(NSDictionary *))handler { if (!handler) return; pthread_mutex_lock(&_mutex); // 按异常类型分组存储回调 NSNumber *typeKey = @(type); if (!_callbackDict[typeKey]) { _callbackDict[typeKey] = [NSMutableArray array]; } [_callbackDict[typeKey] addObject:handler]; pthread_mutex_unlock(&_mutex); } /// 上报异常(触发回调+APM上报) - (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context { // 1. APM平台上报(模拟:实际对接公司APM,如Bugly/云监控) NSLog(@"【APM上报】异常类型:%lu,上下文:%@", type, context); // 2. 触发注册的回调(主线程执行,避免UI操作崩溃) dispatch_async(dispatch_get_main_queue(), ^{ pthread_mutex_lock(&self->_mutex); NSNumber *typeKey = @(type); NSArray *handlers = self->_callbackDict[typeKey]; pthread_mutex_unlock(&self->_mutex); for (void(^handler)(NSDictionary *) in handlers) { if (handler) { handler(context); } } }); } - (void)dealloc { pthread_mutex_destroy(&_mutex); } #pragma mark - 类方法封装(简化调用) + (void)registerCallbackForType:(WXExceptionType)type handler:(void (^)(NSDictionary *))handler { [[self sharedManager] registerCallbackForType:type handler:handler]; } + (void)reportExceptionWithType:(WXExceptionType)type context:(NSDictionary *)context { [[self sharedManager] reportExceptionWithType:type context:context]; } @end ``` 使用的地方 ```Objective-C - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; // 1. 初始化价格标签 self.priceLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 200, 200, 40)]; self.priceLabel.font = [UIFont systemFontOfSize:18]; [self.view addSubview:self.priceLabel]; // 2. 模拟业务数据:价格数组(长度3,索引0-2) self.priceArray = @[@"99.9", @"199.9", @"299.9"]; // 标记数组所属业务模块(关键:用于回调筛选) self.priceArray.bizModule = @"goodsPrice"; // 3. 注册数组越界异常回调(仅关注价格模块) [WXExceptionManager registerCallbackForType:WXExceptionTypeArrayOutOfBounds handler:^(NSDictionary *context) { // 筛选:仅处理“价格数组”的越界异常 if ([context[WXExceptionContextBizModuleKey] isEqualToString:@"goodsPrice"]) { NSLog(@"【业务降级】商品价格数组越界,触发UI兜底"); // 生产环境:展示友好提示(而非错误值) self.priceLabel.text = @"价格获取失败"; // 可选:触发其他降级逻辑(如隐藏价格、显示默认价) [self triggerPriceFallback]; } }]; // 4. 模拟异常场景:访问索引3(越界) [self testPriceArrayOutOfBounds]; } /// 模拟价格数组越界访问 - (void)testPriceArrayOutOfBounds { // 访问索引3(数组长度3,正常索引0-2) id price = [self.priceArray objectAtIndex:3]; // 正常场景:显示价格;异常场景:price是NSNull,显示兜底 if ([price isKindOfClass:[NSNull class]]) { self.priceLabel.text = @"价格获取失败"; } else { self.priceLabel.text = [NSString stringWithFormat:@"¥%@", price]; } } /// 价格降级逻辑(可选) - (void)triggerPriceFallback { // 比如:隐藏优惠券、显示默认包邮等 NSLog(@"【降级】隐藏优惠券模块,显示默认包邮文案"); } @end ``` 核心优势: - 99% 的低概率异常由全局兜底处理(返回 NSNull+UI 显示 --) - 仅核心业务场景(价格、支付)需写少量回调逻辑,避免冗余 效果,类似针对核心的业务场景做专项化监控和优化。 ### 方案 3:静态分析 + CI/CD 前置拦截(从源头消灭异常) 比运行时 Hook 更优雅的是 **“提前拦截”**。通过静态分析工具(Clang Static Analyzer、OCLint、自定义 LLVM 插件),在代码提交/编译阶段检测出: - 数组越界风险(如array[index]中 index 未做长度校验) - 字典 key 为 nil(如dict[nil]) - 分母为 0(如a / b中 b 未判 0) - 强制类型转换风险(如(NSNumber *)nil) 落地方式: - 集成到 CI/CD Pipeline,代码提交时触发静态分析,有风险则阻断合入 - 通过编写 LLVM 插件,就可以在 Xcode 有问题的代码上实时提示 “数组越界风险”“字典 key 可能为 nil”。比如 效果:线上低概率异常的根源被提前消灭,问题无法逃逸到现线上,业务层几乎无需处理这类异常(因为代码本身就没有漏洞) 关于如何编写 LLVM 插件,做到在 Xcode 中实时展示代码中存在的问题,可以查看:[LLVM 插件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.102.md) 这篇文章。 ### 方案 4:轻量级熔断 / 降级(适合核心业务场景) 对于 “万分之一” 但影响大的异常(如支付金额计算、商品价格),采用「熔断策略」: 第一次出现异常:上报 + 降级为默认值(如价格显示 “--”); 短时间内多次出现(如 1 分钟内 > 3 次):触发熔断,建议:展示兜底 UI,提示安抚用户,稍等片刻后尝试(起码不会发生稳定的 crash 或者业务异常) 熔断状态上报 APM,研发收到告警后优先修复。 ### 方案 5:语义化默认值 + 业务无感知适配(美团 / 饿了么实践) 返回**业务可识别的语义化空值**,并在 UI 层做统一适配: | 异常场景 | 兜底值 | UI 层统一处理 | 业务层感知 | | ---------------- | -------- | --------------- | ---------- | | 数组越界 | NSNull | 显示 “--” | 无 | | 字典 key 为 nil | NSNull | 显示 “暂无数据” | 无 | | 分母为 0 | INFINITY | 显示 “计算异常” | 无 | | 字符串转数字失败 | NSNull | 显示 “--” | 无 | **实现方式**: - 封装全局 UI 工具类(如`WXUILabel+Safe.h`),重写`setText:`方法: ```objective-c - (void) safe_setText:(id)text { if (text == [NSNull null] || text == nil) { self.text = @"--"; } else if ([text isKindOfClass:[NSNumber class]] && isinf([text doubleValue])) { self.text = @"计算异常"; } else { self.text = [text description]; } } ``` 业务层只需使用`safe_setText:`,无需为每个异常场景写判断逻辑。 ## 四、最佳实践 1. **开发环境零容忍**:通过静态分析、自定义 LLVM 插件 + 运行时 Hook,让问题在测试阶段暴露,比如异常发生时,通过日志打印案发现场数据,或者在 Xcode 面板上可视化的在有问题的代码地方显示 error,及时 crash 掉,阻塞正常流程,以防止开发偷懒不去及时修复,产生技术债,同时也防止问题逃逸到线上。 2. **生产环境软兜底**:不要一刀切,做到可配置。业务方在配置平台可以针对不同业务域的配置,决定是否开启兜底展示(某些业务域的架构师可能更严格,需要开发在 debug 阶段解决这些问题,线上如果出现就统计为 crash)。如果开启,那么安全气垫就返回语义化默认值(而非无意义的 0),全局 UI 统一处理,业务层零侵入; 3. **可观测驱动修复**:APM 平台统计异常频率 / 影响度,优先处理高频高影响的异常; 4. **核心场景按需关注**:仅对价格、支付等核心场景注册回调,非核心场景无需处理。 虽然机制和策略已经制定了,但是执行还是靠人,难以保证100%解决问题。所以开发解决通过 lint 和 crash 可以拦截到大部分问题,但是逃逸到线上的部分问题,还是需要安全气垫去 cover,线下无限毕竟去清空问题 + 少数逃逸到线上的问题通过安全气垫去 cover 就可以保证相对全面的安全守护。 ## 五、平台做些什么 获取到 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:[打造一个通用、可配置、多句柄的数据上报 SDK](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md)),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如**波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)**。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。 线上异常上报后,APM 平台需提供: - 异常频率(如 “数组越界” 仅 1/10000); - 影响用户数(如仅影响 10 个用户); - 业务上下文(如 “商品详情页 - 价格数组”); - 调用栈 + 设备信息。 **核心逻辑**: - 低频率、低影响的异常(如 1/10000,影响 10 用户):暂时无需业务层处理,研发排期修复即可; - 高频率、高影响的异常(如 1/100,影响 1000 用户):触发告警,研发紧急修复,业务层临时加兜底。 这样业务开发无需为 “万分之一” 的异常写逻辑,仅需处理 “高频高影响” 的异常。 ## 总结 “最佳实践”部分选择的方案既避免了 “安全气垫返回错误值导致业务异常”,又解决了 “业务开发为低概率异常写冗余逻辑” 的问题,是网易、腾讯、阿里等大厂的通用实践 ——**优雅的方案不是 “兜底所有异常”,而是 “让异常可被提前发现、可被观测、最小化影响”**,即使少数逃逸到线上的,再由安全气垫去兜底解决(解决了展示问题、异常上报和案发现场还原问题),一个好的 APM 平台不光是告诉你又问题,还会告诉你哪里有问题。