Swift数组线程安全问题:调用dropFirst时越界崩溃求助
解决Swift数组并发读写导致的Range崩溃问题
你猜的完全没错!这个崩溃的核心原因就是数组的并发读写操作——Swift的Array本身不是线程安全的,当你在一个线程(比如数据源的回调线程)不断追加数据,同时又在另一个线程执行dropFirst裁剪操作时,两个操作会同时修改数组的内部状态,导致索引计算混乱,最终触发那个Can't form Range with upperBound < lowerBound的致命错误。
下面给你几个实用的解决方案,按场景选择就行:
1. 用串行GCD队列保证操作原子性
这是最经典且兼容性最好的方案,创建一个串行队列,把所有对数组的读写操作都放到这个队列里执行,确保同一时间只有一个操作能修改数组:
// 定义串行队列和数组 private let dataProcessingQueue = DispatchQueue(label: "com.yourapp.data.queue") private var dataArray: [YourDataType] = [] // 代理方法接收数据 func dataSourceDidReceiveNewData(_ data: YourDataType) { dataProcessingQueue.async { [weak self] in guard let self = self else { return } self.dataArray.append(data) // 超过阈值时裁剪旧数据 let maxCount = 50000 if self.dataArray.count > maxCount { // 直接计算要删除的数量,用removeFirst更高效(原地修改) self.dataArray.removeFirst(self.dataArray.count - maxCount) // 如果你习惯用dropFirst也可以,本质是创建新数组: // self.dataArray = self.dataArray.dropFirst(self.dataArray.count - maxCount) } } } // 如果需要读取数组(比如在主线程更新UI),也要通过串行队列 func fetchCurrentData(completion: @escaping ([YourDataType]) -> Void) { dataProcessingQueue.async { [weak self] in let currentData = self?.dataArray ?? [] DispatchQueue.main.async { completion(currentData) } } }
这个方案的好处是不需要额外的锁逻辑,GCD会帮你处理好串行执行的顺序,避免并发冲突。
2. 用NSLock手动加锁保护数组
如果你不想依赖GCD,也可以用NSLock来包裹所有读写操作,确保同一时间只有一个操作能访问数组:
private let dataLock = NSLock() private var dataArray: [YourDataType] = [] func dataSourceDidReceiveNewData(_ data: YourDataType) { dataLock.lock() defer { dataLock.unlock() } // defer确保锁一定会被释放,避免死锁 dataArray.append(data) let maxCount = 50000 if dataArray.count > maxCount { dataArray.removeFirst(dataArray.count - maxCount) } } // 读取数组时同样需要加锁 func getCurrentData() -> [YourDataType] { dataLock.lock() defer { dataLock.unlock() } return dataArray }
注意一定要用defer来解锁,哪怕代码中间抛出异常,锁也能正常释放,避免死锁风险。
3. 用Swift Actor实现线程安全(iOS 13+/macOS 10.15+)
如果你的项目支持较新的系统版本,用Actor是更现代的方案——Swift的Actor天然保证内部状态的线程安全,不需要手动处理锁或队列:
// 用Actor封装数组和操作 actor DataStore { private var dataArray: [YourDataType] = [] private let maxDataCount = 50000 func addNewData(_ data: YourDataType) { dataArray.append(data) if dataArray.count > maxDataCount { dataArray.removeFirst(dataArray.count - maxDataCount) } } func getAllData() -> [YourDataType] { return dataArray } } // 使用方式 let dataStore = DataStore() func dataSourceDidReceiveNewData(_ data: YourDataType) { // 在Task里调用Actor的方法,自动处理线程切换 Task { await dataStore.addNewData(data) } } func useDataForUI() { Task { let currentData = await dataStore.getAllData() // 切回主线程更新UI DispatchQueue.main.async { // 处理数据、更新UI逻辑 } } }
Actor会自动把所有对内部状态的操作放到串行执行的上下文里,彻底避免并发冲突。
为什么之前的代码会崩溃?
举个简单的场景:
- 线程A刚判断
dataArray.count > 50000,准备计算要删除的数量(count - 50000) - 这时候线程B又追加了一条数据,导致
count变大;或者线程B同时执行了裁剪操作,导致count变小 - 线程A用之前的
count计算出要删除的数量,去执行dropFirst时,数组的实际长度已经和计算时不一样了,最终导致Range的上下界出现颠倒,触发致命错误。
只要保证数组的读写操作是串行执行的,就能彻底解决这个问题。
内容的提问来源于stack exchange,提问作者sree_iphonedev
相关产品推荐
相关产品推荐

