3

I have the following properties defined on a UIViewController:

@property (nonatomic, strong) UIButton *strongButton;
@property (nonatomic, weak) UIButton *weakButton;

These properties are not set via Interface Builder (i.e. they will always remain nil as long as I don't set them explicitly in code).

I also added a category on UIButton to know exactly when it is dealloced:

@implementation UIButton (test)
- (void)dealloc { NSLog(@"Dealloc called."); }
@end

I have the following code in the viewDidLoad of the UIViewController:

self.strongButton = [[UIButton alloc] init];
self.weakButton = self.strongButton;
NSAssert(self.weakButton != nil, @"A: Weak button should not be nil.");
NSLog(@"Setting to nil");
self.strongButton = nil;
NSLog(@"Done setting to nil");
NSAssert(self.weakButton == nil, @"B: Weak button should be nil.");

When I run that code, it fails on the second assertion (B). The log shows:

  • Setting to nil
  • Done setting to nil
  • * Assertion failure in -[ViewController viewDidLoad]
  • * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'B: Weak button should be nil.'

However, when I comment out the first assertion:

self.strongButton = [[UIButton alloc] init];
self.weakButton = self.strongButton;
//NSAssert(self.weakButton != nil, @"A: Weak button should not be nil.");
NSLog(@"Setting to nil");
self.strongButton = nil;
NSLog(@"Done setting to nil");
NSAssert(self.weakButton == nil, @"B: Weak button should be nil.");

The code runs fine with this in the log:

  • Setting to nil
  • Dealloc called.
  • Done setting to nil

Notice how the dealloc is not being called at the proper time in the first scenario.

Why is that first NSAssert causing this weird behavior? Is this a bug or am I doing something wrong?

(I am on iOS 6.1)

Senseful
  • 86,719
  • 67
  • 308
  • 465
  • 2
    I could be wrong (not an expert on this stuff), but I don't think ARC cleans up the weak pointers until the autorelease pool drains. So `self.weakButton` won't be `nil` until the current run loop ends. – Aaron Brager Jul 30 '13 at 00:34
  • If you made a second button for the second assertion, I bet it'd work. – Aaron Brager Jul 30 '13 at 00:35
  • @AaronBrager: If I move the second assertion to `viewDidAppear` it works correctly. However, I don't think that explains why the second scenario does work. – Senseful Jul 30 '13 at 00:43
  • weak has nothing in particular to do with autorelease pools. What you may be thinking of is that if a strongly referenced object is autoreleased, that strong reference won't go away until the autorelease pool removes it while draining. – Catfish_Man Jul 30 '13 at 00:48
  • Do you get the same behavior when you use 'assign' instead of 'weak'? The reason I'm asking is Apple says "For declared properties, you should use assign instead of weak;" in the Transitioning to ARC Release Notes. BTW, your problem sounds like a bug to me. – Fred Jul 30 '13 at 01:21

1 Answers1

7

Reading a weak variable may cause the pointed-to object to be retained and autoreleased. The object will then remain alive at least as long as the current autorelease pool.

In your case, your first NSAssert() reads the weak variable. The button object is retained and autoreleased. Setting self.strongButton=nil does not cause the button to be deallocated, because it is still alive in the autorelease pool, so the weak variable does not become nil.

When you comment out the NSAssert(), the weak variable is no longer read, so the button is not retained and autoreleased, so it does in fact die when you set self.strongButton=nil.

Greg Parker
  • 7,972
  • 2
  • 24
  • 21