首页 > 解决方案 > 使用 NSFetchedResultsController 的 CoreData 性能。UITableView 在数据导入期间冻结

问题描述

我的应用程序类似于(几乎相同的代码): https ://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed

该应用程序在启动时从 JSON 加载并创建/更新 100000 多条记录到 CoreData。

应用程序仅包含带有单个 tableview 的屏幕(带有来自导入的数据)。NSFetchedResultsController 用于在表格视图中显示数据。

当应用程序在数据库中导入数据时,UI 冻结,我尝试滚动表格视图,即使在小滚动时它也会冻结。

我在导入期间看到主线程的负载为 100%,因此 UI 将冻结。

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
}()

视图控制器代码:

// 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()
    }
}

标签: iosswiftdatabasecore-datafreeze

解决方案


所有 UI 都在主线程中执行,因此您可以尝试使用全局队列来执行此获取。

我认为具有 .utility QoS 的全局队列足以解决这个问题。

这是一个示例:

DispatchQueue.global(qos: .utility).async {
            //do the fetch here
}

全局队列是并发的,实用程序 QoS(服务质量)告诉 GCD 此任务不应在主线程中执行,并且 .async 使执行不会阻塞此全局队列。

如果不完全清楚,我可以为您发布最完整的示例代码:)

祝你好运!


推荐阅读