Reputation: 53
I went through many discutions and subjects about CoreData, but I keep getting the same problem.
Here's the context : I have an application which have to do several access to CoreData. I decided, for simplifying, to declare a serial thread specifically for access (queue.sync for fetching, queue.async for saving). I have a structure that is nested three times, and for recreating the entire structure, I fetch subSubObject, then SubObject and finally Object
But sometimes (like 1/5000 recreation of "Object") CoreData crash on fetching results, with no stack trace, with no crash log, only a EXC_BAD_ACCESS (code 1)
Objects are not in cause, and the crash is weird because all access are done in the same thread which is a serial thread
If anyone can help me, I will be very grateful !
Here's the structure of the code :
private let delegate:AppDelegate
private let context:NSManagedObjectContext
private let queue:DispatchQueue
override init() {
self.delegate = (UIApplication.shared.delegate as! AppDelegate)
self.context = self.delegate.persistentContainer.viewContext
self.queue = DispatchQueue(label: "aLabel", qos: DispatchQoS.utility)
super.init()
}
(...)
public func loadObject(withID ID: Int)->Object? {
var object:Object? = nil
self.queue.sync {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
fetchRequest.predicate = NSPredicate(format: "id == %@", NSNumber(value: ID))
do {
var data:[NSManagedObject]
// CRASH HERE ########################
try data = context.fetch(fetchRequest)
// ###################################
if (data.first != nil) {
let subObjects:[Object] = loadSubObjects(forID: ID)
// Task creating "object"
}
} catch let error as NSError {
print("CoreData : \(error), \(error.userInfo)")
}
}
return object
}
private func loadSubObjects(forID ID: Int)->[Object] {
var objects:[Object] = nil
self.queue.sync {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Name")
fetchRequest.predicate = NSPredicate(format: "id == %@", NSNumber(value: ID))
do {
var data:[NSManagedObject]
// OR HERE ###########################
try data = context.fetch(fetchRequest)
// ###################################
if (data.first != nil) {
let subSubObjects:[Object] = loadSubObjects(forID: ID)
// Task creating "objects"
}
} catch let error as NSError {
print("CoreData : \(error), \(error.userInfo)")
}
}
return objects
}
(etc...)
Upvotes: 5
Views: 3060
Reputation: 8563
TL;DR: get rid of your queue, replace it with an operation queue. Run fetches on the main thread with viewContext
do writing in one synchronous way.
There are two issues. First is that managedObjectContexts are not thread safe. You cannot access a context (neither for reading or for writing) except from the single thread that it is setup to work with. The second issue is that you shouldn't be doing multiple writing to core-data at the same time. Simultaneous writes can lead to conflicts and loss of data.
The crash is cause by accessing viewContext
from a thread that is not the main thread. The fact that there is a queue ensuring that nothing else is accessing core data at the same time doesn't fix it. When core-data thread safety is violated core-data can fail at any time and in any way. That means that it may crash with hard to diagnose crash reports even at points in the code where you are on the correct thread.
You have the right idea that core-data needs a queue to work well when saving data, but your implementation is flawed. A queue for core-data will prevent write conflicts caused by writing conflicting properties to an entity at the same time from different context. Using NSPersistentContainer
this is easy to set up.
In your core-data manager create a NSOperationQueue
let persistentContainerQueue : OperationQueue = {
let queue = OperationQueue.init();
queue.maxConcurrentOperationCount = 1;
return queue;
}()
And do all writing using this queue:
func enqueueCoreDataBlock(_ block: @escaping (NSManagedObjectContext) -> Swift.Void){
persistentContainerQueue.addOperation {
let context = self.persistentContainer.newBackgroundContext();
context.performAndWait {
block(context)
do{
try context.save();
} catch{
//log error
}
}
}
}
For writing use enqueueCoreDataBlock:
which will give you a context to use and will execute every block inside the queue so you don't get write conflicts. Make sure that no managedObject leave this block - they are attached to the context which will be destroyed at the end of the block. Also you can't pass managedObjects into this block - if you want to change a viewContext
object you have to use the objectID
and fetch in the background context. In order for the changes to be seen on the viewContext
you have to add to your core-data setup persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
For reading you should use the viewContext
from the main thread. As you are generally reading in order to display information to the user you aren't gaining anything by having a different thread. The main thread would have to wait for the information in any event so it is faster just the run the fetch on the main thread. Never write on the viewContext
. The viewContext
does not use the operation queue so writing on it can create write conflicts. Likewise you should treat any other contexts that you create (with newBackgroundContext
or with performBackgroundTask
) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer
's performBackgroundTask
had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.
Upvotes: 9