28 KiB
移动端的“安全气垫”
安全气垫是端上一个老生常谈的话题,技术方案已经是好多年前的产物。唯一的问题是不同公司对于安全气垫在拦截到异常的时候,“做与不做”策略的判断不同。比如数组索引越界的时候要返回一个错误位置的值吗?会不会产生业务异常?要一刀切吗?那么本文就尝试回答下这个问题
一、一个经典的场景
Weex 源码中,设计了一个 WXThreadSafeMutableDictionary。在字典操作的地方使用锁:
- (void)setObject:(id)anObject forKey:(id<NSCopying>)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 的写法
// 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,影响用户体验,但是比如数组本该越界,现在却不越界:
- 唯一能做的就是返回一个错误的值,比如数组长度为3,访问4,现在不 crash,返回了 0 的值,那是不是产生了业务异常?比如商品价格
- 不 crash,也不返回错误位置的值,类似给一个回调,告诉业务方出现了异常,可以做一些业务层面的提醒或者配置(比如开发阶段商品卡片的价格 Label 显示:商品价格获取错误,数组越界),同时产生的异常案发现场信息和其他的一些数据会上报,用于 APM 平台去分析和定位。
但这也产生一个问题,类似数组越界的场景,可能10000次里面9999次都正常,只有1次异常,业务开发为了这万分之一出现的异常,还需要写一些异常处理的逻辑(比如商品卡片展示价格获取错误,数组越界)。那字典的 key 为 nil 呢?除法的分母为0呢?诸如此类,类似乐观锁和悲观锁的场景
二、核心原则
要解决「安全气垫防崩溃但引发隐性业务异常」「低概率异常导致业务开发冗余逻辑」的核心矛盾,业界的优雅方案核心思路是:「环境差异化策略」+「分层兜底 + 语义化默认值」+「可观测驱动的轻量处理」,既避免线上 Crash,又最小化业务侵入,同时保证问题可被发现和修复。
开发阶段让问题 “炸出来”,生产阶段让问题 “软落地”。从源头减少线上低概率异常的发生,业务开发无需为 “万分之一” 的异常写冗余逻辑
| 环境 | 核心目标 | 策略 |
|---|---|---|
| 开发 / 测试 | 提前暴露问题,杜绝上线 | 「零容忍」:直接 Crash + 详细上下文 |
| 生产 | 避免 Crash + 可观测 + 最小业务影响 | 「软兜底」:语义化默认值 + 全量上报 |
三、多个方案
方案1:环境差异化 + 开发强感知(网易大白/腾讯 Bugly 核心思路)
对 NSArray、NSDictionary、NSNumber 等基础类做运行时 Hook,区分环境处理异常: NSArray+DWSafeHook.m
// 核心逻辑:
// 1. Method Swizzling Hook数组核心读写方法
// 2. Debug环境:越界直接Crash+详细上下文,强制研发修复
// 3. Release环境:拦截崩溃+上报异常+返回语义化空值,避免用户感知
//
#import "NSArray+DWSafeHook.h"
#import <objc/runtime.h>
#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
简化下,逻辑基本为:
// 伪代码: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
#import <Foundation/Foundation.h>
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
#import "WXExceptionManager.h"
#import <pthread/pthread.h>
// 上下文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<NSNumber *, NSMutableArray<void(^)(NSDictionary *)> *> *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
使用的地方
- (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 插件 这篇文章。
方案 4:轻量级熔断 / 降级(适合核心业务场景)
对于 “万分之一” 但影响大的异常(如支付金额计算、商品价格),采用「熔断策略」: 第一次出现异常:上报 + 降级为默认值(如价格显示 “--”); 短时间内多次出现(如 1 分钟内 > 3 次):触发熔断,建议:展示兜底 UI,提示安抚用户,稍等片刻后尝试(起码不会发生稳定的 crash 或者业务异常) 熔断状态上报 APM,研发收到告警后优先修复。
方案 5:语义化默认值 + 业务无感知适配(美团 / 饿了么实践)
返回业务可识别的语义化空值,并在 UI 层做统一适配:
| 异常场景 | 兜底值 | UI 层统一处理 | 业务层感知 |
|---|---|---|---|
| 数组越界 | NSNull | 显示 “--” | 无 |
| 字典 key 为 nil | NSNull | 显示 “暂无数据” | 无 |
| 分母为 0 | INFINITY | 显示 “计算异常” | 无 |
| 字符串转数字失败 | NSNull | 显示 “--” | 无 |
实现方式:
- 封装全局 UI 工具类(如
WXUILabel+Safe.h),重写setText:方法:
- (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:,无需为每个异常场景写判断逻辑。
四、最佳实践
- 开发环境零容忍:通过静态分析、自定义 LLVM 插件 + 运行时 Hook,让问题在测试阶段暴露,比如异常发生时,通过日志打印案发现场数据,或者在 Xcode 面板上可视化的在有问题的代码地方显示 error,及时 crash 掉,阻塞正常流程,以防止开发偷懒不去及时修复,产生技术债,同时也防止问题逃逸到线上。
- 生产环境软兜底:不要一刀切,做到可配置。业务方在配置平台可以针对不同业务域的配置,决定是否开启兜底展示(某些业务域的架构师可能更严格,需要开发在 debug 阶段解决这些问题,线上如果出现就统计为 crash)。如果开启,那么安全气垫就返回语义化默认值(而非无意义的 0),全局 UI 统一处理,业务层零侵入;
- 可观测驱动修复:APM 平台统计异常频率 / 影响度,优先处理高频高影响的异常;
- 核心场景按需关注:仅对价格、支付等核心场景注册回调,非核心场景无需处理。
虽然机制和策略已经制定了,但是执行还是靠人,难以保证100%解决问题。所以开发解决通过 lint 和 crash 可以拦截到大部分问题,但是逃逸到线上的部分问题,还是需要安全气垫去 cover,线下无限毕竟去清空问题 + 少数逃逸到线上的问题通过安全气垫去 cover 就可以保证相对全面的安全守护。
五、平台做些什么
获取到 问题数据是第一步,获取之后问题需要及时上报,需要有一个数据上报系统,数据及时准确、高效的上传到服务端(这就是另一个命题,可以查看这篇文章:打造一个通用、可配置、多句柄的数据上报 SDK),到 APM 后台。后台产生工单,根据业务模块分配到对应的责任人、群组。根据问题的紧急程度,报警策略和提醒频次也不一样,比如波动报警、自定义报警策略等(这也是另一个命题:智能报警策略、波动报警策略、业务线自定义可配置责任人等)。责任人及时解决问题、修改工单状态、热修复或者发布新版本,问题闭环。
线上异常上报后,APM 平台需提供:
- 异常频率(如 “数组越界” 仅 1/10000);
- 影响用户数(如仅影响 10 个用户);
- 业务上下文(如 “商品详情页 - 价格数组”);
- 调用栈 + 设备信息。
核心逻辑:
- 低频率、低影响的异常(如 1/10000,影响 10 用户):暂时无需业务层处理,研发排期修复即可;
- 高频率、高影响的异常(如 1/100,影响 1000 用户):触发告警,研发紧急修复,业务层临时加兜底。
这样业务开发无需为 “万分之一” 的异常写逻辑,仅需处理 “高频高影响” 的异常。
总结
“最佳实践”部分选择的方案既避免了 “安全气垫返回错误值导致业务异常”,又解决了 “业务开发为低概率异常写冗余逻辑” 的问题,是网易、腾讯、阿里等大厂的通用实践 ——优雅的方案不是 “兜底所有异常”,而是 “让异常可被提前发现、可被观测、最小化影响”,即使少数逃逸到线上的,再由安全气垫去兜底解决(解决了展示问题、异常上报和案发现场还原问题),一个好的 APM 平台不光是告诉你又问题,还会告诉你哪里有问题。