简体   繁体   中英

Using block callbacks to the main thread from an NSOperation subclass (ARC)

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. (Eg 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... 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:

- (void)demoMethod {
    __weak id weakSelf = self;
    MySubclass *subclass = [[MySubclass alloc] initWithCallback:^{
        if (!weakSelf) {
            return;
        }
        else {
            // Do whatever the callback does here
        }
    }];

    // Do something with `subclass` here
}

Users of your API should maintain weak references to UIViews and any other objects with this problem. The callback will then no longer keep the UIView around. Within the block, they should assign the weak reference to a strong reference, test that strong reference against nil, and proceed appropriately.

View controllers should be careful not to unnecessarily instantiate their views. Always use [self isViewLoaded] prior to accessing [self view] . (This also applies to [self tableView] in UITableView subclasses, since that's just a correctly typed alias for view .)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM