29

I'm still new to blocks in objective-c and wondering if I have this psuedo code correct. I'm not sure if it's enough to just remove the observer or if i have to call removeObserver:name:object:

-(void) scan {
    Scanner *scanner = [[Scanner alloc] init];
    id scanComplete = [[NSNotificationCenter defaultCenter] addObserverForName:@"ScanComplete" 
                        object:scanner 
                        queue:nil 
                        usingBlock:^(NSNotification *notification){
                            /*
                             do something
                             */
                            [[NSNotificationCenter defaultCenter] removeObserver:scanComplete];
                            [scanner release];
                        }];
    [scanner startScan];
}

Update: I'm receiving intermittent EXC_BAD_ACCESS from this block, so this can't be right.

Juan Catalan
  • 2,299
  • 1
  • 17
  • 23
seanalltogether
  • 3,542
  • 3
  • 26
  • 24

4 Answers4

49

Declare the scanComplete variable before defining the block itself.

The reason why you need to do this is because you're trying to access a variable that doesn't exist within the block at the time of definition since the variable itself has not been assigned yet.

What is EXC_BAD_ACCESS? Well, it's an exception that is thrown when you try to access a reference that doesn't exist. So that is exactly the case in your example.

So if you declare the variable before the block itself, then it should work:

-(void) scan {
    Scanner *scanner = [[Scanner alloc] init];
    __block id scanComplete;
    scanComplete = [[NSNotificationCenter defaultCenter] addObserverForName:@"ScanComplete" 
                        object:scanner 
                        queue:nil 
                        usingBlock:^(NSNotification *notification){
                           /*
                           do something
                           */
                           [[NSNotificationCenter defaultCenter] removeObserver:scanComplete];
                           [scanner release];
                    }];
    [scanner startScan];
}
Jacob Relkin
  • 161,348
  • 33
  • 346
  • 320
  • You need `__block id scanComplete;`, or it will be copied into the block and you will be leaking observers. – hwaxxer Apr 15 '11 at 16:17
  • 4
    In the world of ARC, the comment on using `__block` to avoid capture no longer holds true. What _does_ hold true, is that the `__block` qualifier fixes a fundamental problem: when the block is defined, `addObserverForName:...` has not returned yet, so the value that is captured is `nil` at best (when running under ARC, because of its implicit auto-nil on variable declaration), or **undefined**, trading one BAD_ACCESS for completely undefined behavior, at worst… – danyowdee Nov 26 '11 at 22:20
  • Removing the observer by referring to a local variable from inside the block was always skanky. Store the returned observer token (here, `scanComplete`) as an instance variable; under ARC this should be a `__weak` instance variable to prevent a retain cycle on self. – matt Nov 27 '11 at 02:35
  • 2
    @matt Why? What's skanky about wanting to completely localize the scope of all the moving parts involved with block-based notifications? – CIFilter Sep 07 '12 at 22:53
  • `[scanner release];` should be outside the block – user102008 Mar 25 '13 at 23:12
15

You should not unregister in the register block. Instead, store the token returned from addObserverForName (in this case, your scanComplete) as an instance variable or in a collection that is an instance variable, and unregister later when you're about to go out of existence (e.g. in dealloc). What I do is keep an NSMutableSet called observers. So:

id ob = [[NSNotificationCenter defaultCenter] 
     addObserverForName:@"whatever" object:nil queue:nil 
     usingBlock:^(NSNotification *note) {
        // ... whatever ...
}];
[self->observers addObject:ob];

And then later:

for (id ob in self->observers)
    [[NSNotificationCenter defaultCenter] removeObserver:ob];
self->observers = nil;
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 4
    If you want a one-shot notification, I don't see why you shouldn't be able unregister in the block itself. – ipmcc Nov 03 '12 at 18:09
  • Agreed with the above comment. I think whats missing from this answer is the "why". I think explaining why "you should not unregister in the register block" would be helpful. Especially since Apples own documentation for this API includes recommending unregistering in that block... – Daniel Galasko Jun 18 '18 at 08:38
  • @DanielGalasko The question was how to prevent the crash, and my answer correctly states a way to do that. The accepted answer tackles the problem from the other end, the declaration of `ob`. Both answers are correct ways to prevent the crash. – matt Jun 18 '18 at 12:09
3

Apple Document about this method:

The following example shows how you can register to receive locale change notifications.

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.localeChangeObserver = [center addObserverForName:NSCurrentLocaleDidChangeNotification object:nil
    queue:mainQueue usingBlock:^(NSNotification *note) {

        NSLog(@"The user's locale changed to: %@", [[NSLocale currentLocale] localeIdentifier]);
    }];

To unregister observations, you pass the object returned by this method to removeObserver:. You must invoke removeObserver: or removeObserver:name:object: before any object specified by addObserverForName:object:queue:usingBlock: is deallocated.

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self.localeChangeObserver];
likid1412
  • 963
  • 8
  • 15
-4

The scope of the block doesn't have permission to release the scanner object. If you're not using garbage collection, removing the release and making the scanner autorelease ([[[Scanner alloc] init] autorelease]) should do the trick.

You should also be able to safely move the call to removeObserver outside of the block.

For the case of EXC_BAD_ACCESS: Entering bt in the console window after the application crashes will give you a backtrace, and should inform you where the error occurred.

mralex
  • 86
  • 3