Reputation: 11
the app im developing intermittently throws the above exception on app launch. Here is the stack trace for the thread that triggered the crash.
0 libobjc.A.dylib 0x00000001bbcaa148 object_getClass + 12 (objc-object.h:237)
1 CoreData 0x00000001ae28e6b0 _PFObjectIDFastHash64 + 28 (NSBasicObjectID.m:706)
2 CoreFoundation 0x00000001a84af10c __CFBasicHashRehash + 996 (CFBasicHash.c:477)
3 CoreFoundation 0x00000001a84b2df4 CFBasicHashRemoveValue + 2352 (CFBasicHash.c:1386)
4 CoreFoundation 0x00000001a83d12f8 CFDictionaryRemoveValue + 224 (CFDictionary.c:471)
5 CoreData 0x00000001ae1de8e8 -[NSManagedObjectContext(_NSInternalAdditions) _forgetObject:propagateToObjectStore:removeFromRegistry:] + 120 (NSManagedObjectContext.m:5088)
6 CoreData 0x00000001ae1be450 -[_PFManagedObjectReferenceQueue _processReferenceQueue:] + 864 (NSManagedObjectContext.m:5077)
7 CoreData 0x00000001ae2d6578 __90-[NSManagedObjectContext(_NSInternalNotificationHandling) _registerAsyncReferenceCallback]_block_invoke + 68 (NSManagedObjectContext.m:8825)
8 CoreData 0x00000001ae2ccb18 developerSubmittedBlockToNSManagedObjectContextPerform + 156 (NSManagedObjectContext.m:3880)
9 libdispatch.dylib 0x00000001a80be280 _dispatch_client_callout + 16 (object.m:559)
10 libdispatch.dylib 0x00000001a809a4fc _dispatch_lane_serial_drain$VARIANT$armv81 + 568 (inline_internal.h:2548)
11 libdispatch.dylib 0x00000001a809afe8 _dispatch_lane_invoke$VARIANT$armv81 + 404 (queue.c:3862)
12 libdispatch.dylib 0x00000001a80a4808 _dispatch_workloop_worker_thread + 692 (queue.c:6590)
13 libsystem_pthread.dylib 0x00000001edcd85a4 _pthread_wqthread + 272 (pthread.c:2193)
14 libsystem_pthread.dylib 0x00000001edcdb874 start_wqthread + 8
I use a series of interdependent Operations upon app launch to fetch and clean data from CoreData and permissions initialized via Async calls to the server. I use the below CoreDataManager to handle CoreData saving and fetching.
As I said, this problem occurs seemingly at random. Any help would be appreciated.
/// A manager for interfacing with Core Data.
///
/// This class is a singleton, and the instance is accessible via the `shared` property.
class CoreDataManager {
/// The single shared instance of the `CoreDataManager` class.
static let shared = CoreDataManager()
/// The underlying `NSManagedObjectContext` that is being used by this manager.
let context: NSManagedObjectContext
private let container: NSPersistentContainer
private init() {
container = NSPersistentContainer(name: "FinSiteful")
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
context = container.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
NotificationCenter.default.addObserver(self,
selector: #selector(contextDidSave(_:)),
name: Notification.Name.NSManagedObjectContextDidSave,
object: nil)
}
/// This function executes any time the managed object context is saved.
///
/// - Important: Other uses of the `notification` paramter
///
/// 1. Use `notification.userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>` to get inserted data
/// 2. Use `notification.userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>` to get deleted data
/// 3. Use `notification.userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>` to get updated data
///
/// - Parameter notification: The `Notification` that contains the data updated, inserted, and/or deleted
@objc private func contextDidSave(_ notification: Notification) {
print("CoreDataManager: Context did save")
}
/// Creates a new managed object within the default context.
///
/// This method mostly exists for convenience. Below is an example of how to use this method to create
/// a new `Transaction` object which can be saved into CoreData.
/// ```
/// let trans: Transaction = CoreDataManager.shared.newObject()
/// ```
///
/// - Important: The context must be saved after calling this function in order for the object to be permanently
/// added to the database. Use the `saveContext()` method for this.
///
/// - Returns: A newly created `NSManagedObject`.
func newObject<T>() -> T where T: NSManagedObject {
T(context: self.context)
}
/// Executes the provided fetch request to retrieve data from Core Data.
///
/// - Parameter request: The `NSFetchRequest` to be executed.
/// - Returns: The results of `request`.
func fetch<T>(_ request: NSFetchRequest<T>) -> [T] where T: NSManagedObject {
do {
return try context.fetch(request)
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
/// Convenience wrapper around the context's delete method.
///
/// Specifies that the given object should be removed from its persistent store when changes are committed.
///
/// - Important: The context must be saved after calling this function in order for the changes to permanently take effect.
/// Use the `saveContext()` method for this.
///
/// - Parameter object: The object to be removed.
func delete(_ object: NSManagedObject) {
context.delete(object)
}
/// Saves any changes in the context to the database.
///
/// This method will only attempt to save if there are in fact changes that have been made.
///
/// Under normal circumstances, saving the context should never fail.
/// So, this method produces a `fatalError` in the event of a failure.
func saveContext() {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
More info. This bug specifically occurs during a section of code that runs several API Requests concurrently.
let group = DispatchGroup()
let dispatchSemaphore = DispatchSemaphore(value: 0)
let dispatchQueue = DispatchQueue(label: "transaction-downloads")
dispatchQueue.async {
self.dataCount = 500
while self.dataCount == self.MAXDATACOUNT {
group.enter()
self.saveTransactionsLocal(start: startDate, accessToken: accessToken, accountIDs: accountIDs) { response, error in
if response == .success {
if self.newEnd == self.end {
self.dataCount -= 1
}
self.end = self.newEnd
dispatchSemaphore.signal()
group.leave()
} else {
//Break out of loop
print("PH: saving transactions failed")
completion(.failure, error)
self.dataCount -= 1
}
}
dispatchSemaphore.wait()
}
completion(.success, nil)
}
Long story short, this code downloads data from the API, saves it to CoreData, and if all is well it does so again until the number of objects received from the API is less than 500 (the max you can download). The kicker is this code is called multiple times in a for-loop during an Operation, so in my case this code is run 4 times for the 4 accounts I'm downloading data from.
Upvotes: 1
Views: 1233
Reputation: 1044
When using background context you should use perform block.
For example when saving:
backgroundContext.performAndWait {
guard backgroundContext.hasChanges else { return }
try? backgroundContext.save()
}
Fetching:
backgroundContext.performAndWait {
backgroundContext.fetch(request)
}
Creating:
backgroundContext.performAndWait {
CoreDataObject(context: backgroundContext)
}
You can read here how to use Core Data in background, as stated by Apple:
Because the queue is private and internal to the NSManagedObjectContext instance, it can only be accessed through the perform(:) and the performAndWait(:) methods.
Upvotes: 3