EXC_BAD_ACCESS When saving core data

I'm trying to convert some data, then save it back to Core Data. Sometimes this works fine without an issue, but occasionally I'll get an error

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)

It seems to occur when saving the core data context. I'm having trouble trying to debug it as it doesn't happen on the same object each time and can't reliably recreate the error

Full view code can be found https://pastebin.com/d974V5Si but main functions below

var body: some View {
        VStack {
            // Visual code here
        }
        .onAppear() {
            DispatchQueue.global(qos: .background).async {
                while (getHowManyProjectsToUpdate() > 0) {
                    leftToUpdate = getHowManyProjectsToUpdate()
                    updateLocal()
                }
                if getHowManyProjectsToUpdate() == 0 {
                    while (getNumberOfFilesInDocumentsDirectory() > 0) {
                        deleteImagesFromDocumentsDirectory()
                    }
                    if getNumberOfFilesInDocumentsDirectory() == 0 {
                        DispatchQueue.main.asyncAfter(deadline: .now()) {
                            withAnimation {
                                self.isActive = true
                            }
                        }
                    }
                }
            }
        }
    }

update local function

func updateLocal() {
        autoreleasepool {
            let fetchRequest: NSFetchRequest<Project> = Project.fetchRequest()
            fetchRequest.predicate = NSPredicate(format: "converted = %d", false)
            fetchRequest.fetchLimit = 1
            fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Project.name, ascending: true), NSSortDescriptor(keyPath: \Project.name, ascending: true)]
            
            do {
                let projects = try viewContext.fetch(fetchRequest)
                for project in projects {
                    currentPicNumber = 0
                    currentProjectName = project.name ?? "Error loading project"
                    if let projectMain = project.mainPicture {
                        currentProjectImage = getUIImage(picture: projectMain)
                    }
                    if let pictures = project.pictures {
                        projectPicNumber = pictures.count
                        // Get main image
                        if let projectMain = project.mainPicture {
                            if let imgThumbData = convertImageThumb(picture: projectMain) {
                                project.mainPictureData = imgThumbData
                            }
                        }
                        
                        while (getTotalImagesToConvertForProject(project: project ) > 0) {
                            convertImageBatch(project: project)
                        }
                        
                        project.converted = true
                        saveContext()
                        viewContext.refreshAllObjects()
                    }
                }
            } catch {
                print("Fetch Failed")
            }
        }
    }

convertImageBatch function

func convertImageBatch(project: Project) {
        autoreleasepool {
            let fetchRequestPic: NSFetchRequest<Picture> = Picture.fetchRequest()
            let projectPredicate = NSPredicate(format: "project = %@", project)
            let dataPredicate = NSPredicate(format: "pictureData == NULL")
            
            fetchRequestPic.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [projectPredicate, dataPredicate])
            fetchRequestPic.fetchLimit = 5
            fetchRequestPic.sortDescriptors = [NSSortDescriptor(keyPath: \Picture.dateTaken, ascending: true)]
            
            do {
                let pictures = try viewContext.fetch(fetchRequestPic)
                for picture in pictures {
                    currentPicNumber = currentPicNumber + 1
                    
                    if let imgData = convertImage(picture: picture), let imgThumbData = convertImageThumb(picture: picture) {
                        // Save Converted
                        picture.pictureData = imgData
                        picture.pictureThumbnailData = imgThumbData
                        
                        // Save Image
                        saveContext()
                        viewContext.refreshAllObjects()
                    } else {
                        viewContext.delete(picture)
                        saveContext()
                        viewContext.refreshAllObjects()
                    }
                }
                
            } catch {
                print("Fetch Failed")
            }
        }
    }

And finally saving

    func saveContext() {
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
Answered by DTS Engineer in 846806022

As @deeje pointed out, using viewContext in a background queue, as shown in your following code snippet, does not follow the Core Data multi-threading programming pattern that requires the access to a Core Data context (NSManagedObjectContext) and its objects be from the context’s queue.

DispatchQueue.global(qos: .background).async {
    while (getHowManyProjectsToUpdate() > 0) {
        leftToUpdate = getHowManyProjectsToUpdate()
        updateLocal()
    }
    ...
}

func updateLocal() {
    ...
    viewContext.refreshAllObjects()
}

Violating the programming pattern can trigger random crashes in Core Data, which probably explains the issue you described.

With Xcode, you can use -com.apple.CoreData.ConcurrencyDebug 1 as a launch argument to check if your code has the concurrency issue. Note that the argument has the “-“ prefix. To do that:

  1. Open your project with Xcode.
  2. Click the target in the middle of Xcode’s title bar to show the drop list, and then click “Edit Schema...” to show the schema editing dialog.
  3. Add the argument to the “Arguments Passed on Launch” list, as shown in the attached screenshot.
  4. Run your app and try to reproduce the issue.

If your app hits a concurrency violation, the debugger will halt at the following symbol:

+[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__].

Please give it a try and let me know if this helps.

In general, you can fix this kind of issue by following this pattern:

  1. Always use a queue-based context, meaning that alway use init(concurrencyType:) to create your NSManagedObjectContext instance.

  2. Wrap the code accessing the context and its objects with perform(_:) or performAndWait(_:).

  3. 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:

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Your updateLocal() func has a sort descriptor with two of the same values: fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Project.name, ascending: true), NSSortDescriptor(keyPath: \Project.name, ascending: true)] - does this do anything in particular?

Anyway, you're doing an awful lot work in what should be a simple Core Data update function. Have you tried retrieving from Core Data all the projects that need updating, doing your updates to the various bits, and then saving the context in one go?

Not sure why that was in there twice, shouldn't be. I did originally save outside of the project loop but put it inside due to the crashing, as it would allow it to continue from where it left off.

you're jumping into a background thread, then accessing CoreData using the viewContext, which should only be used on the main thread.

As @deeje pointed out, using viewContext in a background queue, as shown in your following code snippet, does not follow the Core Data multi-threading programming pattern that requires the access to a Core Data context (NSManagedObjectContext) and its objects be from the context’s queue.

DispatchQueue.global(qos: .background).async {
    while (getHowManyProjectsToUpdate() > 0) {
        leftToUpdate = getHowManyProjectsToUpdate()
        updateLocal()
    }
    ...
}

func updateLocal() {
    ...
    viewContext.refreshAllObjects()
}

Violating the programming pattern can trigger random crashes in Core Data, which probably explains the issue you described.

With Xcode, you can use -com.apple.CoreData.ConcurrencyDebug 1 as a launch argument to check if your code has the concurrency issue. Note that the argument has the “-“ prefix. To do that:

  1. Open your project with Xcode.
  2. Click the target in the middle of Xcode’s title bar to show the drop list, and then click “Edit Schema...” to show the schema editing dialog.
  3. Add the argument to the “Arguments Passed on Launch” list, as shown in the attached screenshot.
  4. Run your app and try to reproduce the issue.

If your app hits a concurrency violation, the debugger will halt at the following symbol:

+[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__].

Please give it a try and let me know if this helps.

In general, you can fix this kind of issue by following this pattern:

  1. Always use a queue-based context, meaning that alway use init(concurrencyType:) to create your NSManagedObjectContext instance.

  2. Wrap the code accessing the context and its objects with perform(_:) or performAndWait(_:).

  3. 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:

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

When adding the launch argument it straight away crashes on the fetch in the getHowManyProjectsToUpdate() method with

Thread 9: EXC_BREAKPOINT (code=1, subcode=0x199aa51c4)

The below is printed to the console

CoreData: annotation: Core Data multi-threading assertions enabled.

I've managed to rewrite it to work on a private context and it seems to be working, I've done the conversions several times and so far no crash. Now if I could just speed it up a bit more, its taking about 20 minutes to do 1100 images

EXC_BAD_ACCESS When saving core data
 
 
Q