Recently, I noticed many crashes reported daily for context.save()
in my code. I tried to resolve the issue by refactoring some of the code. My key change is to use context.performAndWait
to fetch Core Data objects instead of passing them around.
I refactored the code to pass NSManagedObjectID
around instead of NSManagedObject
itself because NSManagedObjectID
is thread-safe.
However, even after the refactor, the issue is still. The crash log is attached as below.
I'm confused that it doesn't crash on all devices so I have no clue to find the pattern of the crashes. I also enabled -com.apple.CoreData.ConcurrencyDebug 1
on my local dev box, but seems the error cannot be caught.
The new code (after I refactored) still causes the crash:
private func update(movements: [InferredMovement], from visitOutObjectId: NSManagedObjectID, to visitInObjectId: NSManagedObjectID?) {
let context = PersistenceController.shared.container.viewContext
context.performAndWait {
guard let visitOut = try? context.existingObject(with: visitOutObjectId) as? Visit else {
return
}
var visitIn: Visit?
if let visitInObjectId {
visitIn = try? context.existingObject(with: visitInObjectId) as? Visit
}
visitOut.movementsOut.forEach { context.delete($0) }
visitIn?.movementsIn.forEach { context.delete($0) }
for inferred in movements {
let movement = Movement(context: context)
movement.type = inferred.type
movement.interval = inferred.interval
movement.visitFrom_ = visitOut
movement.visitTo_ = visitIn
}
visitOut.objectWillChange.send()
context.saveIfNeeded()
}
}
Note, saveIfNeeded()
is an extension function implemented as:
extension NSManagedObjectContext {
/// Only performs a save if there are changes to commit.
@discardableResult
public func saveIfNeeded() -> Error? {
guard hasChanges else {
return nil
}
do {
try save()
return nil
} catch {
defaultLogger.error("Core Data Error: Failed to save context")
return error
}
}
}
The code snippet looks safe to me, except that visitOut.objectWillChange.send()
may have the observers do something bad on the objects, which isn't shown in the snippet. You might carefully review that part.
Providing a full crash report may help as well, as it will probably unveil more information. Also, Core Data typically logs more information when hitting an exception, which goes to the sysdiagnose. If you can gather and analyze a sysdiagnose from the device that just reproduced the issue, you will likely find the information. To capture a sysdiagnose, see Profile and Logs.
The part of the crash report in your post, specifically the -[NSObject(NSObject) doesNotRecognizeSelector:]
frame triggered by [NSManagedObjectContext save:]
, indicates an object that isn't of the expected type, which can be:
a. A managed object (NSManagedObject
) that was accessed from a wrong queue. Core Data requires that accessing a managed object must be done from the queue of the managed context (NSManagedObjectContext
). Failing to follow the rule may trigger a random crash like this.
b. A zombie object, which might be something referenced by a managed object and was over-released somewhere.
You can avoid #a by following this pattern:
-
Wrap the code accessing the context and its objects with perform(_:) or performAndWait(_:).
-
If you need to pass Core Data objects across different contexts, use NSManagedObjectID, rather than
NSManagedObject
.
To better understand the Core Data concurrency topic, I recommend you to go through the following technical resources:
You have used -com.apple.CoreData.ConcurrencyDebug 1
as a launch argument to check if your code has the concurrency issue, and so you are on the right track. Be sure to exercise all the code paths though. If you turn the argument on, reproduce the issue, and don't see that the debugger halts at the following symbol: +[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__]
, you should be fine.
For #b, you can investigate zombie objects in your app by following the guidance in Investigating Crashes for Zombie Objects. After triggering the problem by using the Zombies instrument, you can track down the problem by using the information provided by the instrument.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.