This question is similar to this question with automatic reference counting thrown in.
I have an NSOperation subclass that accepts a block argument that is intended as a callback to the main (UI) thread. My original intention was to perform some operation in the background, and then use dispatch_async and the main queue to perform the callback.
Original premise:
@interface MySubclass : NSOperation {
@protected
dispatch_block_t _callback;
}
- (id)initWithCallback:(dispatch_block_t)callback;
@end
@implementation MySubclass
- (void)main
{
// Do stuff
if (![self isCancelled]) {
dispatch_async(dispatch_get_main_queue(), _callback);
}
}
@end
Problems arise when all references to a UIKit object outside the scope of the block are removed. (E.g. a UIViewController is popped off a navigation stack.) This leaves the only reference to the object inside the block, so the object is deallocated when the block is, on the thread where the block is deallocated. Deallocating a UIKit object off the main thread crashes the app with the error message Tried to obtain the web lock from a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread. Crashing now...
As a workaround, I added a __block modifier to the callback ivar, and am using dispatch_sync to make sure everything released is on the main thread.
@interface MySubclass : NSOperation {
@protected
__block dispatch_block_t _callback;
}
- (id)initWithCallback:(dispatch_block_t)callback;
@end
@implementation MySubclass
- (void)main
{
// Do Stuff
if (![self isCancelled]) {
dispatch_block_t block = ^{
_callback();
_callback = nil;
};
// Cover all our bases to prevent deadlock
if ([NSThread isMainThread]) block();
else dispatch_sync(dispatch_get_main_queue(), block);
}
}
@end
I am wondering if there is a better way to accomplish something with this premise. My workaround feels hacky, and I don’t like that I might end up with several operations in my queue all waiting for a turn on the main thread before they can complete.
If you need to ensure the callback runs even if the controller has been popped from the stack, then your workaround is correct.
If, however, you really only need the callback to run if the controller is still around, then it would be simpler to use weak references in the callback to ensure that the block itself doesn’t retain the controller in the first place. It would look something like this: