Xcode报错:NSInvalidArgumentException: -[__NSCFSet addObject:]: attempt to insert nil
Xcode报错:NSInvalidArgumentException: -[__NSCFSet addObject:]: attempt to insert nil

Xcode报错:NSInvalidArgumentException: -[__NSCFSet addObject:]: attempt to insert nil

问题描述

在使用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/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注