У меня недавно сильно болела голова в моем приложении:
В моем приложении каждый из блоков имеет почти 100 атрибутов, которые можно сохранить, и некоторые из них могут обновляться очень часто, даже десятки или большеобновляется на единицу в течение нескольких секунд во время начальной синхронизации.Поскольку объединение их всех в одну таблицу может быть очень неэффективным, и есть некоторые из них, которые с большей вероятностью будут повторно синхронизированы, я попытался разбить блок на 5-6 меньших таблиц в соответствии с функциональностью и возможной частотой обновления, каждая из которыху них есть 8 - 32 поля и еще одно для первичного ключа.Чтобы предотвратить блокировку основного потока, я также создал фоновый контекст для обновления данных.Когда вызывается context.save()
, основной контекст объединяется с фоновым контекстом.
Однако, когда я пытаюсь выполнить пакетное тестирование, скажем, около 50 единиц в таблице, я обнаруживаю, что пользовательский интерфейс все еще серьезнозаблокирован, поскольку основной контекст (который отвечает за выборку макета пользовательского интерфейса в соответствии с атрибутами единиц в базе данных) занят при объединении с интенсивным фоновым обновлением контекста.
Я также обнаружил, что при повторной загрузке данных происходит более 10000 слияний, даже если их всего 50 единиц, я посмотрел на них и обнаружил, что всякий раз, когда вызывается один context.save()
, мое приложение достигает 9 NSManagedObjectContextDidSave
уведомления, что означает, что большая часть слияния на самом деле является избыточной.
Мне также интересно, есть ли лучший способ (как с точки зрения управления контекстом, так и с точки зрения дизайна нормализации таблиц данных) для решения проблемы структуры базы данных: в то время какколичество записей невелико (вероятно, менее 1000), но есть достаточно много полей для обслуживания.Техника манипулирования огромными записями уже достаточно развита, чтобы повысить эффективность, но что касается работы с огромными полями, я могу найти очень ограниченные ресурсы, не говоря уже о ресурсах в CoreData.
Не уверен, если периодически переходить на сохранение контекстаЭто хорошая идея, потому что это приложение Bluetooth, и данные могут приходить и уходить в любое время, в любом потоке, поэтому точность мгновенных данных в локальной БД имеет решающее значение для принятия правильных действий.Тем не менее, изменения не будут приняты, если я не передам их через context.save()
сразу.
Подводя итог, здесь можно задать два вопроса:
Почему несколько NSManagedObjectContextDidSave
сработало, а context.save()
вызывается только один раз?
При работе с такого рода структурой базы данных (с большим количеством полей в записи), есть лилучшая модель, которая может обеспечить лучший компромисс между опытом пользовательского интерфейса и точностью данных?
Кстати, для улучшения производительности пользовательского интерфейса я использую NSFetchedResultsController
, но обнаружил, что он может сделатьочень мало, если в моих таблицах есть отношения, поэтому я помещаю в основную сущность наиболее значимые атрибуты, которые выбирает NSFetchedResultsController, однако это мало помогает, так как основной контекст все еще занят объединением.
Мой код для управления основными данными записан следующим образом:
// MARK: - Example on saving a NSManagedObject in background thread
class ExampleRepository : CoreDataStack {
func updateRecordToDbInBackground(uuid: String, values: [String: Data]) {
let ctx = getBackgroundContext()
ctx.perform {
guard let resultId = self.fetchByUuid(uuid: uuid, context: ctx) else {
self.addRecordToDb(uuid: uuid, values: values, context: ctx)
self.saveContext(context: ctx, method: #function)
return
}
let result = ctx.object(with: resultId)
_ = self.updateRecordToDb(uuid: uuid, values: values, managedObject: result, context: ctx)
self.saveContext(context: ctx, method: #function)
}
}
func fetchByUuid(uuid: String, context: NSManagedObjectContext) -> NSManagedObjectID? {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: ENTITY_NAME)
fetchRequest.predicate = NSPredicate(format: "SELF.uuid = %@", uuid)
do {
return try context.fetch(fetchRequest).first?.objectID
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
return nil
}
func updateRecordToDb(uuid: String, values: [String: Data], managedObject: NSManagedObject?, context: NSManagedObjectContext) -> NSManagedObject {
let registerObj = managedObject ?? getManagedObject(context: context)
registerObj.setValue(uuid, forKey: FIELD_NAME_UUID)
let convertedValues: NSMutableDictionary = [:]
values.forEach { e in
convertedValues.addEntries(from: e.key: e.value])
}
registerObj.setValuesForKeys(convertedValues as! [String: Data])
return registerObj
}
func addRecordToDb(uuid: String, values: [String: Data], managedObject: NSManagedObject?, context: NSManagedObjectContext) -> NSManagedObject {
// add is generally similar to updateRecoredToDb, but adding some relationships to other entities
}
}
// MARK: - Core data stack
class CoreDataStack {
init(){
NotificationCenter.default.addObserver(self, selector: #selector(self.contextDidSaveContext), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil)
}
deinit{
NotificationCenter.default.removeObserver(self)
}
var _context: NSManagedObjectContext {
return SingletonContext.sharedInstance._context
}
var _backgroundContext: NSManagedObjectContext {
return SingletonContext.sharedInstance._backgroundContext
}
func saveContext(context: NSManagedObjectContext, method: String) {
guard context.hasChanges else {
return
}
context.perform {
do {
try context.save()
print("Context called from \(method) saved!")
} catch let error as NSError {
print("Could not save context called from \(method), Error: \(error), \(error.userInfo)")
}
}
}
@objc func contextDidSaveContext(notification: NSNotification) {
// The question is here: This func is called nine times when the context.save() were called once.
let sender = notification.object as! NSManagedObjectContext
if sender === self._context {
print("******** Merge background context with main ********")
self._backgroundContext.perform {
self._backgroundContext.mergeChanges(fromContextDidSave: notification as Notification)
}
}
else if sender === self._backgroundContext {
print("******** main main context with background ********")
self._context.perform {
self._context.mergeChanges(fromContextDidSave: notification as Notification)
}
}
}
}
class SingletonContext {
static let sharedInstance = SingletonContext()
var _persistentStoreCoordinator: NSPersistentStoreCoordinator {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.persistentStoreCoordinator
}
lazy var _context: NSManagedObjectContext = {
let ctx = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType)
ctx.persistentStoreCoordinator = self._persistentStoreCoordinator
return ctx
}()
lazy var _backgroundContext: NSManagedObjectContext = {
let ctx = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType)
ctx.persistentStoreCoordinator = self._context.persistentStoreCoordinator
return ctx
}()
}