Swift - Application crash when using two different OperationQueues with KVO
Asked Answered
S

1

7

I'm getting two type of information with JSON and I'm adding "operations" to 2 different Operation Queues Classes with addObserver(forKeyPath:"operations"...). In the function observeValue I'm checking if operationQueue1.operations.isEmpty and then I refresh my information in UI. I'm doing the same thing with if else with operationQueue2, but when the 2 operations are started in sometime the application crash with error message: *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <AppName.ViewController 0x102977800> for the key path "operations" from <AppName.OperationQueue1 0x1c4a233c0> because it is not registered as an observer.' I don't have problem when only 1 operation is started. Any suggestions?

func getInfo1(){//runned in viewDidLoad
  operationQueue1.addObserver(forKeyPath:"operations"...)
  operationQueue1.dataTask(URL:"..."....){
      DispatchQueue.main.async{
  NotificationCenter.default.postNotification(NSNotification.Name(rawValue: "NewDataReceived1", userInfo:infoFromTheWebsite)
      }
  }
}

func NewDataReceived1(){
  here I add the information to arrays to be loaded in tableView1
}

HERE IS THE CODE FOR 2ND INFO WHICH IS THE SAME

override func observeValue(forKeyPath keyPath: String?, ....){
        if(object as? operationQueue1 == operationQueue1Class && keyPath == "operations" && context == context1){
             if(operationQueue1.operations.isEmpty){
                  DispatchQueue.main.async{
                        operationQueue1..removeObserver(self, forKeyPath:"operations")
                        Timer.scheduled("refreshingTableInformation1") 
                   }
             }
        }else if(operationQueue2....){
             SAME AS OPERATION 1, BUT USING DIFFERENT FUNC TO REFRESH TABLE INFORMATION AND THE TABLES ARE DIFFERENT
        }else{
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
}

func refreshingTableInformation1(){
     tableView1.reloadData()
     Timer.scheduled("getInfo1", repeat:false)
}

func refreshingTableInformation2(){
     tableView2.reloadData()
     Timer.scheduled("getInfo2", repeat:false)
}

Sometimes it works 10 secs and crash and sometimes works for more than 60 seconds and then crash...

Sibeal answered 13/12, 2017 at 0:31 Comment(4)
Like the error says, you are trying to remove a view controller from observing the operations keypath of your OperationQueue1 even though the view controller isn't registered as an observer. This could probably occur if you the reference to the vc changed. Post your operation queue's code and where you are adding the observer.Swordfish
I'm adding the observer before the 2 requests and they are locked in infinite circle. When I update the information in the UI I set the observer again and I'm starting request to get the information again.Sibeal
Your code isn't very clear. Anyway, why not putting an observer on the array that collects 'NewDataReceived1' instead? would be easier to handle. Also, if you have sync issues, it's better to use libs such as Alamofire for your http requests. This would spare you a lot of trouble.Rokach
I'm not gonna add the observer in NewDataReceived1, because I'm using operationQueue1.dataTask 3 times, so that means I'm gonna set 3 observers...Sibeal
L
4

Your code here is vulnerable to race conditions. Consider the following scenario:

  1. getInfo1() is called, which adds an operation to operationQueue1.

  2. The operation completes, which means your KVO observation is called. The queue is now empty, so your observation schedules removal of your observer on the main dispatch queue.

  3. Now, before the operation you've submitted to the main queue is able to run, something else calls getInfo1(), which adds a new operation to operationQueue1, which completes before the operation you queued in step 2 has had the chance to run (hey, maybe the main queue was busy with something; it's easy for this to happen since it's a serial queue).

  4. Your observation for the first call of getInfo1() is called again while the queue is empty, causing another deregister block to be submitted to the main queue.

  5. The two deregister blocks finally get to execute on the main queue. The second one crashes the program, since you've already deregistered your observer.

You could probably fix this problem (assuming the code doesn't have more problems of this nature) by using Swift 4's block-based observers instead, and setting the observer to nil instead of explicitly deregistering it. However, I propose that KVO is the wrong tool for what you're trying to do. As the instructions for the old "Crystal Quest" game used to say, it's a bit like using an anti-aircraft gun to kill a mosquito.

From what I can see from the code above, it looks like you're using KVO just to schedule a notification for when an operation or a group of operations you submit to the queue finishes. Depending on what your dataTask method actually does, here's what I'd do instead:

  • If you submit only one operation: set the operation's completionBlock property to a closure that refreshes your table information.

  • If you submit more than one operation: Make a new BlockOperation which refreshes your table information, and call addDependency on that operation with every other operation you submit to the queue. Then, submit that operation.

This will provide you with a much cleaner and more trouble-free way of monitoring completion for your tasks. And since you don't need the queue to completely empty anymore, you might not even have to use two separate queues anymore, depending on what else you're doing with them.

Ladybug answered 16/12, 2017 at 3:1 Comment(3)
I'm using KVO, because I have dynamic table views and if the website doesn't return me info I'm not creating the table viewSibeal
@BogdanBogdanov Could you please be more specific about what exactly you are doing? I do not understand why deferring the creation of a table view would necessitate using KVO on the operation queue instead of just doing it in a completion operation.Ladybug
I agree with @CharlesSrstka, my first thought was a race condition, and particularly number 4 on his list above. With the newer tableView.beginUpdates() and .endUpdates() or performBatchUpdates(_:completion:) makes it possible to update the tableView from the changeHandler of a block-observer. note that observe(_:options:changeHandler:) calls addObserver_:forKeyPath:options:context:) and removeObserver(_:forKeyPath:context:) when needed.Pegmatite

© 2022 - 2024 — McMap. All rights reserved.