How to properly deal with a deallocated delegate of a queued nsoperation
In my current project, several view controllers (like vc
) spawn NSOperation objects (like operation
) that are executed on a static NSOperationQueue. While the operation is waiting or running, it will report back to the view controller via delegation (operation.delegate = vc
, assigned not retained).
These operations can take a while though, and in the mean time the app can dealloc the view controller (by popping them of a navigation controller's stack).
So far everything is intentional. The class containing the static NSOperationQueue has a way to get back at the operations, therefore the view controllers do not retain them. They're just alloc/init/autoreleased and put on the queue.
Now this also causes the problem. After the view controller deallocates, any calls to the NSOperation's spirited delegate will cause a bad access violation. From what I understand, it is not possible to check whether an object at a pointer has been deallocated, as stated in this question.
One fix I can think of is retaining the operation and setting the operation.delegate to nil on dealloc. But that'd be my least popular fix, for it would introduce a lot of extra ivars/properties to keep track of.
My question therefore is, are there other ways to work around this problem and if so, could you sketch one here?
Cheers,
EP.SOLUTION: The approach that worked out best for me was a slight variation to Guiliano's answer:
Implementing every delegate protocol in the queue manager is not feasible (20+ different protocols with 50+ methods), so I kept the direct delegate assignments. What I did change was the class making the assign call. This used to be the class (and delegate) that created the request, but now it is offloaded to the queue manager.
The queue manager, next to assigning the delegate to the operation, also holds a secondary mutable dictionary to keep track of the delegate/operation pairs.
Every delegate instance calls a
[QueueManager invalidateDelegate:self]
method on deallocation, which then looks up the request that belonged to the delegate and nils it. The dictionary operation/delegate pair is then also delete开发者_运维问答d to allow proper deallocation of the operation.Lastly with KVO observing the
isFinished
property of each operation, the mutable dict is kept clean, to make sure that all operation retain counts actually deallocate after they're finished.
Thanks Guiliano for providing the hint to using KVO for cracking this!
I would suggest to review your architecture and move the delegate to the class (assume QueueManager) that manages the queue instead of having a delegate in each operation:
Create a QueueManagerDelegate that implements the method(s) you need to notify the viewControllers
In QueueManager add a KVO observer for the isFinished property of each NSOperation (do this before adding the operation to the queue ;))
In the callback of the KVO call the delegate method(s) you need only if delegate is != nil
Add an invalidate method to QueueManager and call this method in the dealloc method of your UIViewController(s)
-(void)invalidate { self->delegate = nil; }
in case you need a refresh on KVO: Kvo programming guide
The best advice here is to review the architecture of the app to avoid such situations. However, if there current code can't be changed some-why, you can use NSNotificationCenter. Every time your view controller is deallocated you can post a notification, this notifications must be caught by the NSOperationQueue holder, simple foreach cycle in the notification handler to nil the delegate for a deallocated view controller. Should do the trick.
You should also be checking to ensure that any delegates, if non-nil, are also able to respond to a message from the operation completion. You do this using the respondsToSelector
function that all NSObject subclasses posess.
In my projects, I've abstracted this checking into a category on NSObject that lets me safely call delegates with an arbitrary number of object arguments:
- (void) dispatchSelector:(SEL)selector
target:(id)target
objects:(NSArray*)objects
onMainThread:(BOOL)onMainThread {
if(target && [target respondsToSelector:selector]) { // Do your delegate calls here as you please } }
You can see the full example here: https://github.com/chaione/ChaiOneUtils/blob/master/Categories/NSObject-Dispatch.m
精彩评论