3

There is a similar question here, which doesn't explain exactly what I want: Objective C Blocks as Async-callbacks & BAD ACCESS

I have a view controller, which calls a service with an async callback. The callback is done using a block, which references variables on the view controller to populate them.

It looks like so:

- (void) loadData {
    __block MyViewController *me = self;
    [self.service executeWithCompletion:^(NSArray *result, NSError *error) {
        if (!error) {
            me.data = result;  
        }
    }];
}

However, if I dealloc the view controller, 'me' is then badly accessed by the callback.

What is the simplest way of making 'me' NULL? If i put it as an iVar, it then brings back the circular reference... i think?

I think I'm missing something obvious....

Thanks

Community
  • 1
  • 1
bandejapaisa
  • 26,576
  • 13
  • 94
  • 112

3 Answers3

5

Are you targeting iOS 5.0 or later (or Mac OS X 10.7 or later)? If so, you can use ARC and a __weak variable (instead of a __block one). This will automatically zero out when the referenced object is deallocated. Your code would look like

- (void)loadData {
    __weak MyViewController *me = self;
    [self.service executeWithCompletion:^(NSArray *result, NSError *error) {
        if (!error) {
            MyViewController *strongMe = me; // load __weak var into strong
            if (strongMe) {
                strongMe.data = result;
            }
        }
    }];
}

If you need support for an older OS then you need to find a different solution. One solution is to just go ahead and let the block retain self. If the service is guaranteed to execute the completion block (and then release it), this will only produce a temporary cycle that will break automatically when the completion block is run. Alternatively if you have some way to cancel the service (in a way that guarantees the block cannot be called after the cancellation), you can stick with the __block and just be sure to cancel the service in your -dealloc. There's other alternatives too but they're more complicated.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • I can't migrate my whole project to ARC yet. It's too large. Anyway, I'm targeting iOS 4. Yes, I'm looking at adding a cancellation to the service. Can't I somehow put the __block variable at the iVar scope, and nill/NULL it in dealloc? This is referred to in the post that was similar to mine that I referenced. – bandejapaisa Jun 05 '12 at 21:27
  • @bandejapaisa: How would you do it before blocks? You would also need a way to cancel it, or have the same crashing problem. – user102008 Jun 05 '12 at 21:33
  • @KevinBallard yeah - you're right. As any iVar, is still going to point to self, and I get the retain cycle. – bandejapaisa Jun 05 '12 at 21:34
  • @user102008 before blocks.... I would have used a delegate, and I would be nilling my delegate. – bandejapaisa Jun 05 '12 at 21:37
  • 1
    @bandejapaisa: right, so you need to do something similar to "nilling the delegate". you need to somehow tell it to no longer call the block when it's done – user102008 Jun 05 '12 at 21:39
  • Yup I know. I came here for the 'somehow'. Thanks. – bandejapaisa Jun 06 '12 at 08:45
  • Hey @Kevin! I was wondering, is there actually any way to "find out" if the creating routine has been dealloced?? Say you are NOT using __weak TheClass *myself=self. So the block is not putting a retain on the theClass. So it's perfectly possible that "theClass" has been dealloc'd. How the heck could you find that out? I've noticed if you try to use if(!self) inside the block, it simply crashes. ie if theClass has indeed been dealloc'd, the statement if(!self) just crashes the phone/simulator. In fact - is it totally impossible to find that out??? Thanks! – Fattie Nov 12 '13 at 14:12
  • @JoeBlow: Without `__weak`, you cannot determine if a given object has been deallocated. – Lily Ballard Nov 12 '13 at 18:31
  • Hi Kevin .. so: just to clarify for anyone reading, with __weak, it will be retained (and, obviously! you can't find out if it has been dealloc, as it won't be dealloc). So setting that aside, we are not using __weak: You're telling me, in fact there IS NO WAY to tell if an object has been dealloc'd? if so - THANK YOU SO MUCH! I have the feeling "I investigated for hours, came to a conclusion, and now a top expert has confirmed my findings!" :) Some people very mistakenly think you can look at the "self"-but that is WRONG, correct? A pointer WON'T tell you if it has been dealloc. THANKS!! – Fattie Nov 13 '13 at 15:59
  • In short Kevin, notice here for instance, me and Sergio: http://stackoverflow.com/questions/19512400/how-to-fire-block-event-in-objective-c-when-uiviewcontroller-dealloc in fact, "who is right"? (My testing suggests what Sergio says is incorrect, and [I may misunderstand what you are saying right here] what you say here seems to suggest Sergio is incorrect.) Thank you so much, this is a puzzler. – Fattie Nov 13 '13 at 16:01
  • @JoeBlow: "with __weak, it will be retained" What do you mean? `__weak` means the object *isn't* retained, and further, any attempt to load the value in a `__weak` variable will return nil if the object has deallocated (or has begun deallocating, which is an important distinction). So if you use `__weak`, you can determine if any given value has been deallocated by checking the value of the `__weak` variable (that you initialized to the value). – Lily Ballard Nov 13 '13 at 23:14
  • @JoeBlow: I think the fundamental misunderstanding here is that you believe that `__weak id foo = self` will retain `self`. It won't. If I understand your linked question correctly, sergio is correct. – Lily Ballard Nov 13 '13 at 23:16
  • Kevin, thanks a million. I will step back and thoroughly investigate __weak. Thanks again. – Fattie Nov 14 '13 at 10:38
1

I did a combination of things above from the suggestions. Including nilling the blocks. Although, my objects are still not getting released immediately. i.e. I'd put a breakpoint on dealloc of MyViewController, and without the __block variable it would get called at a much later point in time (probably due to the async connection) and sometimes not at all.

The code is fairly complex - so I imagine there are other things going on for it to not work as suggested above.

What I have also done, is used Mike Ash's MAZeroingWeakRef, which i guess is the same as using __weak - which @KevinBallard suggested.

Below is how I've implemented it, and it appears to be working. Dealloc is called immediately on disposal of the view controller, which i want. And I can't get it to crash... and with the log comment that i've put in, I can already see that I'm dodging bullets.

- (void) loadData {
    __block MAZeroingWeakRef *zeroWeakRef = [[MAZeroingWeakRef alloc] initWithTarget:self];
    [zeroWeakRef setCleanupBlock: ^(id target) {
        [zeroWeakRef autorelease];
    }];
    [self.service executeWithCompletion:^(NSArray *result, NSError *error) {
        MyViewController *me = [zeroWeakRef target];
        if (!me) {
            DULog(@"dodged a bullet");
        }
        if (!error) {
            me.data = result;  
        }
    }];
}
bandejapaisa
  • 26,576
  • 13
  • 94
  • 112
0

Is there a real retain cycle problem that you're trying to avoid? Is there a reason that self should not simply be retained until -executeWithCompletion: completes? Is there any real chance that it won't complete?

So long as it really will eventually complete (even with failure) and so long as it releases the block after invoking it (perhaps by setting a property to nil), then the retain cycle will eventually be broken and all will be well.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • Well there could be a flaw in my design. But there is definite retain cycles. The network call returns fairly quickly, and it's immediately obvious that the view controller doesn't get released (dealloc isn't called) - and then running it through instruments leaks and cycles confirms where the problem is. Making it __block definitely fixes this retain cycle, again instruments confirms this. – bandejapaisa Jun 06 '12 at 08:44
  • As I say, you have to release the block after invoking it in whatever code is behind `-executeWithCompletion:`. Try showing that code. – Ken Thomases Jun 06 '12 at 08:48
  • If you look at my code snippet, MyViewController retains 'service', and the block, which is given to 'service', retains MyViewController (if I don't use __block). So there is the cycle. – bandejapaisa Jun 06 '12 at 08:52
  • But it's a temporary cycle with an external event (the completion of the network communication) to break it. There's no reason the cycle should be a real problem. – Ken Thomases Jun 06 '12 at 08:54
  • It's definitely not temporary. It hangs around permanently. I see what you're getting out now with your other comment. When I call the service, as i'm calling an async network service, I store the block into an iVar: savedCompletionBlock - this is where the retain problem is. When the network call returns, i execute savedCompletionBlock(); So you are suggesting, that after i execute this, I call self.savedCompletionBlock = nil;. Which will eliminate the temporary retain cycle. I'll give it a go. Although I have also just implemented a cancel method, which does the nil, which is working too. – bandejapaisa Jun 06 '12 at 09:13
  • Yes, that's what I'm suggesting. – Ken Thomases Jun 06 '12 at 09:18