调试NSMutableArray调用subarrayWithRange时的EXC_BAD_ACCESS错误
根因定位方向
该偶发崩溃90%以上是多线程并发读写非线程安全的NSMutableArray导致,不稳定复现、本地测试难命中是并发问题的典型特征,常见触发场景有两种:
- 方法执行过程中,其他线程同时修改/释放
self.events,导致两次读取self.events.count的结果不一致,或self.events本身被释放为野指针 - 其他线程在你读取数组元素的同时移除了对应位置的元素,或数组存储的元素本身已经被提前释放,访问时触发野指针解引用
排查方案
- 检查
self.events属性的修饰符:如果是nonatomic,多线程下同时读写属性会直接返回未初始化/已释放的无效指针 - 全局搜索所有修改
self.events的代码(增删元素、重新赋值、清空操作),确认是否和getEvents:方法的调用线程不同,存在并发操作的可能 - 开启Xcode僵尸对象检测:编辑Scheme→Run→Diagnostics页面勾选
Zombie Objects,复现崩溃时会直接打印被提前释放的对象类型,确认是数组本身还是数组内元素问题 - 在方法内加线程校验:如果业务要求所有事件操作都在主线程执行,可在
getEvents:开头加断言NSAssert([NSThread isMainThread], @"不能在后台线程调用getEvents:"),命中时直接触发断点定位非法调用线程
修复方案
- 首先优化
getEvents:方法实现,避免多次读取属性导致的中间值被篡改:
- (nullable NSArray<id> *)getEvents:(NSUInteger)numberOfEvents { // 局部持有当前数组,避免方法执行过程中self.events被释放/重新赋值 NSArray *localEvents = self.events; NSUInteger currentCount = localEvents.count; if (currentCount == 0 || numberOfEvents == 0) { return nil; } NSUInteger fetchCount = MIN(numberOfEvents, currentCount); NSLog(@"min = %ld", (long)fetchCount); return [localEvents subarrayWithRange:NSMakeRange(0, fetchCount)]; }
- 给
self.events加线程安全防护:所有对self.events的读写操作都放到同一个串行队列执行,从根本上避免并发问题,参考实现如下:
// 类扩展内定义串行队列 @property (nonatomic, strong) dispatch_queue_t eventQueue; // 初始化时创建队列 - (instancetype)init { self = [super init]; if (self) { _eventQueue = dispatch_queue_create("com.yourapp.eventOperationQueue", DISPATCH_QUEUE_SERIAL); _events = [NSMutableArray array]; } return self; } // 读操作加队列防护 - (nullable NSArray<id> *)getEvents:(NSUInteger)numberOfEvents { __block NSArray *result = nil; dispatch_sync(self.eventQueue, ^{ NSUInteger currentCount = self.events.count; if (currentCount == 0 || numberOfEvents == 0) { return; } NSUInteger fetchCount = MIN(numberOfEvents, currentCount); result = [self.events subarrayWithRange:NSMakeRange(0, fetchCount)]; }); return result; } // 所有写操作(比如新增事件)也同样放到该队列执行 - (void)addEvent:(id)event { dispatch_async(self.eventQueue, ^{ [self.events addObject:event]; }); }
- 如果业务允许,可将对外暴露的
events属性改为不可变NSArray类型,内部维护可变数组,对外只返回不可变拷贝,避免外部直接修改容器触发意外问题。
内容的提问来源于stack exchange,提问作者stefanosn
相关产品推荐
相关产品推荐

