У меня есть приложение, похожее на (почти тот же код): https://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed
Приложение загружает и создает / обновляет 100000+ записей из JSON в CoreData при запуске.
Только приложениесостоит из экрана с одним табличным представлением (с данными из импорта) .NSFetchedResultsController используется для отображения данных в табличном представлении.
Когда приложение импортирует данные в базу данных, пользовательский интерфейс замораживается, я пытаюсь прокрутить табличное представление, оно также зависает даже при небольших прокрутках.
Я вижу 100%загрузка основного потока во время импорта, поэтому пользовательский интерфейс будет зависать.
Контексты PrivateQueueConcurrencyType (используемые для пакетного сохранения bg) имеют mainQueueConcurrencyType в качестве родительского.mainQueueConcurrencyType имеет privateQueueConcurrencyType в качестве родителя с подключенным к нему persistentStoreCoordinator.(как описано https://medium.com/soundwave-stories/core-data-cffe22efe716)
Можно ли как-то сделать процесс импорта в фоновом режиме (в другом потоке) и не блокировать основной поток в этом случае?
Как практически не повлиять на основной поток при большом импорте?
Код:
Основной стек данных
private lazy var managedObjectModel: NSManagedObjectModel = {
guard let modelURL = Bundle.main.url(forResource: "Earthquakes", withExtension: "momd") else {
fatalError("Unable to Find Data Model")
}
guard let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Unable to Load Data Model")
}
return managedObjectModel
}()
private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
let fileManager = FileManager.default
let storeName = "test.sqlite"
let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let persistentStoreURL = documentsDirectoryURL.appendingPathComponent(storeName)
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: persistentStoreURL,
options: nil)
} catch {
fatalError("Unable to Load Persistent Store")
}
return persistentStoreCoordinator
}()
lazy var parentContext: NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.persistentStoreCoordinator = persistentStoreCoordinator
return moc
}()
lazy var context: NSManagedObjectContext = {
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.parent = parentContext
return moc
}()
/**
Fetches the earthquake feed from the remote server, and imports it into Core Data.
Because this server does not offer a secure communication channel, this example
uses an http URL and adds "earthquake.usgs.gov" to the "NSExceptionDomains" value
in the apps's info.plist. When you commmunicate with your own servers, or when
the services you use offer a secure communication option, you should always
prefer to use https.
*/
func fetchQuakes(completionHandler: @escaping (Error?) -> Void) {
// Create a URL to load, and a URLSession to load it.
guard let jsonURL = URL(string: earthquakesFeed) else {
completionHandler(QuakeError.urlError)
return
}
let session = URLSession(configuration: .default)
// Create a URLSession dataTask to fetch the feed.
let task = session.dataTask(with: jsonURL) { data, _, error in
// Alert the user if no data comes back.
guard let data = data else {
completionHandler(QuakeError.networkUnavailable)
return
}
// Decode the JSON and import it into Core Data.
do {
// Decode the JSON into codable type GeoJSON.
let decoder = JSONDecoder()
var geoJSON = try decoder.decode(GeoJSON.self, from: data)
geoJSON.quakePropertiesArray.append(contentsOf: geoJSON.quakePropertiesArray)
geoJSON.quakePropertiesArray.append(contentsOf: geoJSON.quakePropertiesArray)
geoJSON.quakePropertiesArray.append(contentsOf: geoJSON.quakePropertiesArray)
geoJSON.quakePropertiesArray.append(contentsOf: geoJSON.quakePropertiesArray)
print(geoJSON.quakePropertiesArray.count)
// Import the GeoJSON into Core Data.
try self.importQuakes(from: geoJSON)
} catch {
// Alert the user if data cannot be digested.
completionHandler(QuakeError.wrongDataFormat)
return
}
completionHandler(nil)
}
// Start the task.
task.resume()
}
/**
Imports a JSON dictionary into the Core Data store on a private queue,
processing the record in batches to avoid a high memory footprint.
*/
private func importQuakes(from geoJSON: GeoJSON) throws {
guard !geoJSON.quakePropertiesArray.isEmpty else { return }
// Create a private queue context.
//let taskContext = persistentContainer.newBackgroundContext()
let taskContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
taskContext.parent = context
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// Set unused undoManager to nil for macOS (it is nil by default on iOS)
// to reduce resource requirements.
taskContext.undoManager = nil
// Process records in batches to avoid a high memory footprint.
let batchSize = 256
let count = geoJSON.quakePropertiesArray.count
// Determine the total number of batches.
var numBatches = count / batchSize
numBatches += count % batchSize > 0 ? 1 : 0
for batchNumber in 0 ..< numBatches {
// Determine the range for this batch.
let batchStart = batchNumber * batchSize
let batchEnd = batchStart + min(batchSize, count - batchNumber * batchSize)
let range = batchStart..<batchEnd
// Create a batch for this range from the decoded JSON.
let quakesBatch = Array(geoJSON.quakePropertiesArray[range])
// Stop the entire import if any batch is unsuccessful.
if !importOneBatch(quakesBatch, taskContext: taskContext) {
return
}
}
}
/**
Imports one batch of quakes, creating managed objects from the new data,
and saving them to the persistent store, on a private queue. After saving,
resets the context to clean up the cache and lower the memory footprint.
NSManagedObjectContext.performAndWait doesn't rethrow so this function
catches throws within the closure and uses a return value to indicate
whether the import is successful.
*/
private func importOneBatch(_ quakesBatch: [QuakeProperties], taskContext: NSManagedObjectContext) -> Bool {
var success = false
// taskContext.performAndWait runs on the URLSession's delegate queue
// so it won’t block the main thread.
taskContext.performAndWait {
// Create a new record for each quake in the batch.
for quakeData in quakesBatch {
// Create a Quake managed object on the private queue context.
guard let quake = NSEntityDescription.insertNewObject(forEntityName: "Quake", into: taskContext) as? Quake else {
print(QuakeError.creationError.localizedDescription)
return
}
// Populate the Quake's properties using the raw data.
do {
try quake.update(with: quakeData)
} catch QuakeError.missingData {
// Delete invalid Quake from the private queue context.
print(QuakeError.missingData.localizedDescription)
taskContext.delete(quake)
} catch {
print(error.localizedDescription)
}
}
// Save all insertions and deletions from the context to the store.
if taskContext.hasChanges {
do {
try taskContext.save()
context.performAndWait {
try? context.save()
}
} catch {
print("Error: \(error)\nCould not save Core Data context.")
return
}
// Reset the taskContext to free the cache and lower the memory footprint.
taskContext.reset()
}
success = true
}
return success
}
// MARK: - NSFetchedResultsController
/**
A fetched results controller delegate to give consumers a chance to update
the user interface when content changes.
*/
weak var fetchedResultsControllerDelegate: NSFetchedResultsControllerDelegate?
/**
A fetched results controller to fetch Quake records sorted by time.
*/
lazy var fetchedResultsController: NSFetchedResultsController<Quake> = {
// Create a fetch request for the Quake entity sorted by time.
let fetchRequest = NSFetchRequest<Quake>(entityName: "Quake")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
// Create a fetched results controller and set its fetch request, context, and delegate.
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: nil, cacheName: nil)
controller.delegate = fetchedResultsControllerDelegate
// Perform the fetch.
do {
try controller.performFetch()
} catch {
fatalError("Unresolved error \(error)")
}
return controller
}()
Код ViewController:
// MARK: - UITableViewDataSource
extension QuakesViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "QuakeCell", for: indexPath) as? QuakeCell else {
print("Error: tableView.dequeueReusableCell doesn'return a QuakeCell!")
return QuakeCell()
}
guard let quake = dataProvider.fetchedResultsController.fetchedObjects?[indexPath.row] else { return cell }
cell.configure(with: quake)
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataProvider.fetchedResultsController.fetchedObjects?.count ?? 0
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension QuakesViewController: NSFetchedResultsControllerDelegate {
/**
Reloads the table view when the fetched result controller's content changes.
*/
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.reloadData()
}
}