问题描述
在使用Core Data插入数据的过程中,发现Xcode报错:
CrashReportError: ERdepot crashed due to an uncaught exception
ERdepot crashed due to an uncaught exception `NSGenericException`. Reason: *** Collection <__NSCFSet: 0x60000ccf0ae0> was mutated while being enumerated..
经过查询了解到Xcode报错的原因是由于向 Set 类型中插入了 nil 值,也就是经典的 Objective-C 报错:
NSInvalidArgumentException: -[__NSCFSet addObject:]: attempt to insert nil
在Swift代码中添加调试语句:
// 插入新记录
for i in 0..<goldPointDataCount {
/// print("进入第\(i+1)轮遍历")
print("进入第\(i+1)轮遍历")
let newYahooGoldPrice = YahooPoint(context: context)
print("最新的一条数据,symbol:\(urlName),timestamp:\(goldPointDataFirst.timestamp[i]),close:\(goldPoint.close[i] ?? 0.0),hight:\(goldPoint.high[i] ?? 0.0),low:\(goldPoint.low[i] ?? 0.0),open:\(goldPoint.open[i] ?? 0.0),volume:\(goldPoint.volume[i] ?? 0.0)")
/// print("创建 YahooPoint 对象,urlName:\(urlName)")
print("插入symbol")
newYahooGoldPrice.symbol = urlName
print("插入timestamp")
newYahooGoldPrice.time = goldPointDataFirst.timestamp[i]
print("插入close")
newYahooGoldPrice.close = goldPoint.close[i] ?? 0.0
print("插入high")
newYahooGoldPrice.high = goldPoint.high[i] ?? 0.0
print("插入low")
newYahooGoldPrice.low = goldPoint.low[i] ?? 0.0
print("插入open")
newYahooGoldPrice.open = goldPoint.open[i] ?? 0.0
print("插入volume")
newYahooGoldPrice.volume = goldPoint.volume[i] ?? 0.0
print("完成第\(i+1)轮遍历")
}
print("保存插入的 Gold 图表数据")
try context.save()
发现Xcode输出如下:
...
最新的一条数据,symbol:SZ,timestamp:2025-05-23 02:57:00 +0000,close:10271.560546875,hight:10272.52734375,low:10266.78125,open:10266.78125,volume:32870008.0
插入symbol
插入timestamp
插入close
插入high
插入low
插入open
插入volume
完成第88轮遍历
因此,判断在进入下一轮遍历的过程中卡住了,没有到输出“保存插入的 Gold 图表数据”的步骤。
现在怀疑问题可能出在数组越界、数组不一致或者数据本身出现异常(如nil、NaN、inf)导致Core Data报错。
为了进一步排查原因,添加了关于检查数组越界访问的代码:
func safeDouble(_ value: Double?) -> Double {
guard let v = value, v.isFinite else {
print("value:\(value ?? 0)不是正常数值")
return 0.0
}
return v
}
let timestamps = goldPointDataFirst.timestamp
let closes = goldPoint.close
let highs = goldPoint.high
let lows = goldPoint.low
let opens = goldPoint.open
let volumes = goldPoint.volume
print("本次插入的数据为:\(goldPointDataFirst)")
print("timestamps最大长度: \(timestamps.count)")
print("opens最大长度: \(opens.count)")
print("highs最大长度: \(highs.count)")
print("lows最大长度: \(lows.count)")
print("closes最大长度: \(closes.count)")
print("volumes最大长度: \(volumes.count)")
/// 获取 Yahoo 图表数据的条目
let count = min(
timestamps.count,
closes.count,
highs.count,
lows.count,
closes.count,
volumes.count
)
/// print("进入第\(i+1)轮遍历")
if i + 1 < count {
print("进入第\(i+1)轮遍历")
print("下一轮一条数据,symbol:\(urlName),timestamp:\(goldPointDataFirst.timestamp[i+1]),close:\(goldclose[i+1] ?? 0.0),hight:\(goldPoint.high[i+1] ?? 0.0),low:\(goldPoint.low[i+1] ?? 0.0),open:\(goldopen[i+1] ?? 0.0),volume:\(goldPoint.volume[i+1] ?? 0.0)")
} else {
print("下一轮没有数据")
}
使用safeDouble检测数值的有效性,同时计算各字段的总和,取最小的字段长度进行遍历,防止数组越界。
再次运行,发现Xcode在解码的过程中出现了问题:
---1---
Key 'CodingKeys(stringValue: "timestamp", intValue: nil)' not found: No value associated with key CodingKeys(stringValue: "timestamp", intValue: nil) ("timestamp").
codingPath: [CodingKeys(stringValue: "chart", intValue: nil), CodingKeys(stringValue: "result", intValue: nil), _CodingKey(stringValue: "Index 0", intValue: 0)]
经过多次尝试,发现报错是偶发的,因此推断问题可能是在调用汇率接口时,接口的字段会因为市场变动,个别字段会变成nil,如报错的timestamp字段,因此在解码的过程中出现问题,并导致使用Core Data新增数据步骤出现失败。
异常断点排查
在查看调试输出,无法发现具体的报错代码,因此在Xcode中添加“异常断点”,来捕获崩溃发生的代码位置,关于异常断点的知识点,详见《Xcode断点》。
这里添加的是“Exception Breakpoint”(异常断点)。
断点1
第一步捕获的是访问已经释放或非法的内存地址报错。
Thread 5: EXC_BAD_ACCESS (code=1, address=0xfffffffffffffff8)

定位代码为:
for result in pointResults {
context.delete(result) // 定位崩溃代码
}
问题原因可能是在遍历pointResults,每次删除一个实体对象时,系统还在原来的数组中继续遍历,但是因为数组引用可能被更改或其元素已被释放。
最终导致内存错乱或iterator无效,从而抛出EXC_BAD_ACCESS。
解决方案为,先复制数组再删除:
let safeResults = pointResults // 复制一份,避免遍历时被修改
for result in safeResults {
context.delete(result)
}
或者使用forEach用法:
pointResults.forEach { context.delete($0) }
这两种写法都可以打断引用链、避免边遍历边修改的问题。
断点2
再次运行Xcode,发现Xcode仍然崩溃,崩溃信息为:
Thread 8: EXC_BREAKPOINT (code=1, subcode=0x194fc9ca8)
定位代码行在context.save()。
// 插入新记录
for i in 0..<count {
let newYahooGoldPrice = YahooPoint(context: context)
newYahooGoldPrice.symbol = urlName
newYahooGoldPrice.time = timestamps[i]
newYahooGoldPrice.close = safeDouble(closes[i])
newYahooGoldPrice.high = safeDouble(highs[i])
newYahooGoldPrice.low = safeDouble(lows[i])
newYahooGoldPrice.open = safeDouble(opens[i])
newYahooGoldPrice.volume = safeDouble(volumes[i])
}
print("保存插入的 Gold 图表数据")
try context.save() // 定位崩溃代码
EXC_BREAKPOINT (code=1, subcode=0x194fc9ca8) 错误,通常说明程序在运行过程中遇到了一个断点式的崩溃,可能由于以下几种常见情况之一导致:
1、拼写错误:变量名拼错,导致访问非法内存。
2、字符串插值语法错误。
3、在后台线程中创建Core Data对象,使用主线程的context引发EXC_BREAKPOINT。
我推测是第三种情况导致的问题,之前在代码中添加了线程检测:
print("当前线程:\(Thread.current)")
输出的代码为:
当前线程:<NSThread: 0x30016bb00>{number = 8, name = (null)}
Thread.current.number == 1通常代表主线程(Main Thread)。
其他数字(比如 number = 8)一般就是后台线程或其他辅助线程。
因此,尝试使用后台上下文backgroundContext插入数据:
context.perform {
// 你的遍历与 save() 逻辑放这里
}
代码示例:
let context = CoreDataPersistenceController.shared.backgroundContext
context.perform {
do {
// 1. 解码(JSONDecoder.decode) —— 这个可以在外面做
// 2. context.fetch(...)
// 3. if let existing = ...
// 4. let newYahooGoldPrice = Yahoo(context: context)
// 5. 设置属性
// 6. context.delete(...)
// 7. context.save()
} catch {
print("处理 Yahoo 数据时出错:\(error)")
}
}
总结
目前来看主要是上面的两个异常导致的本次报错。
1、遍历删除导致内存错乱。
2、在后台线程中调用主线程的context导致的崩溃。
在调试的过程中,我还添加了一些数值校验,例如:
func safeDouble(_ value: Double?) -> Double {
guard let v = value, v.isFinite else {
if let value = value {
print("value:\(value) 不是正常数值(非有限数)")
} else {
print("value 为 nil")
}
return 0.0
}
print("v:\(v)")
return v
}
if let goldPoint = goldPointDataFirst.indicators.quote.first {
let timestamps = goldPointDataFirst.timestamp
let closes = goldPoint.close
let highs = goldPoint.high
let lows = goldPoint.low
let opens = goldPoint.open
let volumes = goldPoint.volume
/// 获取 Yahoo 图表数据的条目
let count = min(
timestamps.count,
opens.count,
highs.count,
lows.count,
closes.count,
volumes.count
)
// 插入新记录
for i in 0..<count {
let newYahooGoldPrice = YahooPoint(context: context)
newYahooGoldPrice.symbol = urlName
newYahooGoldPrice.time = timestamps[i]
newYahooGoldPrice.close = safeDouble(closes[i])
newYahooGoldPrice.high = safeDouble(highs[i])
newYahooGoldPrice.low = safeDouble(lows[i])
newYahooGoldPrice.open = safeDouble(opens[i])
newYahooGoldPrice.volume = safeDouble(volumes[i])
print("完成第\(i+1)轮遍历")
}
try context.save()
}
一个是通过safeDouble检验数值,防止出现nil等情况,第二个是在遍历数组的时候,通过min获取最小的长度,防止出现数组越界等情况。
相关文章
1、Xcode断点:https://fangjunyu.com/2025/05/26/xcode%e6%96%ad%e7%82%b9/
2、Core Data NSManagedObjectContext的后台上下文backgroundContext:https://fangjunyu.com/2025/03/31/core-data-nsmanagedobjectcontext%e7%9a%84%e5%90%8e%e5%8f%b0%e4%b8%8a%e4%b8%8b%e6%96%87backgroundcontext/