主视图控制器外的多线程进度条并发问题

Multi-threaded progress bar concurrency issue outside the main view controller

我发现了很多在同一个线程和视图控制器中更新进度条的解决方案,但是它们似乎与我的情况不同。

在我的应用程序中,主视图控制器调用 loadIntoCoreData()(在 class MyLoadingService 中实现),它通过另一个线程将数据异步加载到核心数据中。此函数必须不断更新加载百分比(写在 NSUserDefaults.standardUserDefaults() 中)到主线程,以便它可以显示在主视图控制器的进度条上。我曾经在 MainViewController 中使用 while 循环来连续获取当前百分比值,如下所示:

class MainViewController {

    override func viewDidLoad() {
        MyLoadingService.loadIntoCoreData() { result in 

            NSUserDefaults.standardUserDefaults().setBool(false, forKey: "isLoading")
            // do something to update the view

        }
        self.performSelectorInBackground("updateLoadingProgress", withObject: nil)
    } 

    func updatingLoadingProgress() {
        let prefs = NSUserDefaults.standardUserDefaults()
        prefs.setBool(true, forKey: "isLoading")

        // here I use a while loop to listen to the progress value
        while(prefs.boolForKey("isLoading")) {

            // update progress bar on main thread
            self.performSelectorOnMainThread("showLoadingProcess", withObject: nil, waitUntilDone: true)
        }
        prefs.setValue(Float(0), forKey: "loadingProcess")
    }    

    func showLoadingProcess() {
        let prefs = NSUserDefaults.standardUserDefaults()
        if let percentage = prefs.valueForKey("loadingProcess") {
            self.progressView.setProgress(percentage.floatValue, animated: true)
        }
    }
}

并且在函数loadIntoCoreData的class中:

class MyLoadingService {

    let context = (UIApplication.sharedApplication()delegate as! AppDelegate).managedObjectContext!

    func loadIntoCoreData(source: [MyModel]) {
        var counter = 0
        for s in source {
            //load into core data using the class context

            NSOperationQueue.mainQueue.addOperationWithBlock({
                // updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults() 
                // and synchronize it on main queue
            })
            counter++
        }
    }
}

上面的代码可以成功运行进度条,但是经常遇到BAD_ACCESS或者其他一些异常(比如"Cannot update object that was never inserted"),因为核心数据上下文冲突(以为managedObjectContext 似乎没有被主线程触及)。因此,我考虑使用 NSOperationQueue.performSelectorOnMainThread 在每次进入后确认主线程,而不是使用 while 循环监听主线程。因此,我将我的视图控制器作为参数 sender 放入 loadCoreData 并调用 performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true) 但因错误 "unrecognized selector sent to class 'XXXXXXXX'" 而失败。所以我想问一下是否可以在线程之间更新一个UI对象?或者,如何修改我以前的解决方案,以便解决核心数据上下文冲突?任何解决方案表示赞赏。

class MyLoadingService {
    func loadIntoCoreData(sender: MainViewController, source: [MyModel]) {
        var counter = 0
        for s in source {
            //load into core data using the class context

            NSOperationQueue.mainQueue.addOperationWithBlock({
                // updating the value of "loadingProcess" in NSUserDefaults.standardUserDefaults() 
                // and synchronize it on main queue
            })
            NSOperationQueue.performSelectorOnMainThread("updateProgressBar", withObject: sender, waitUntilDone: true)
            counter++
        }
    }
    func updateProgressBar(sender: MainViewController) {
        sender.progressView.setProgress(percentage, animated: true)
    }
}

class MainViewController {
    override func viewDidLoad() {
        MyLoadingService.loadIntoCoreData(self) { result in 

            // do something to update the view
        }
    }
} 

首先,您在以可怕的方式滥用 NSUserDefaults。文档将其描述为...

The NSUserDefaults class provides a programmatic interface for interacting with the defaults system. The defaults system allows an application to customize its behavior to match a user’s preferences. For example, you can allow users to determine what units of measurement your application displays or how often documents are automatically saved. Applications record such preferences by assigning values to a set of parameters in a user’s defaults database. The parameters are referred to as defaults since they’re commonly used to determine an application’s default state at startup or the way it acts by default.

您正在使用它来存储全局变量。

此外,您在不断检查用户默认值的循环中完全滥用了用户的 CPU,并将选择器剪裁到主线程。 "Abuse of the CPU" 甚至无法描述这段代码的作用。

您应该使用 NSProgress 来报告进度。有一个 WWDC 2015 presentation 专门用于使用 NSProgress


关于您的核心数据使用情况。

不幸的是,由于您有意编辑了所有核心数据代码,所以无法说出哪里出了问题。

但是,根据我所见,您可能正在尝试从后台线程使用来自您的应用程序委托的托管对象上下文(它可能仍然使用已弃用的限制策略创建),这是就核心数据而言最高阶。

如果要将数据导入为长 运行 操作,请使用私有上下文,并在后台执行操作。使用 NSProgress 与任何想听的人交流进度。


编辑

Thanks for the advice on my core data context usage. I digged into all the contexts in my code and re-organized the contexts inside, the conflict problem does not happen anymore. As for NSProgress , it's a pity that the WWDC presentation focus on the feature on iOS 9 (while my app must compact on iOS 8 devices). However, even though I use NSProgress, I should still tell the main thread how many data the core data (on another thread) already has, right? How does the thread on NSProgress know the loading progress on my core data thread? – whitney13625

您仍然可以为 iOS8 使用 NSProgress,唯一的区别是您不能显式添加子项,但隐式方法仍然有效,该视频也对此进行了解释。

您真的应该观看整个视频并忘记 iOS9 部分,只是要知道您必须隐式而不是显式添加子项。

此外,this pre-iOS9 blog post 应该可以解决您对此的任何疑问。