8

I was playing around with memory (de)allocation stuff on a simple command line app for Mac OSX 10.7 built using Xcode Version 4.2.1 with ARC enabled, and the default build settings. I can't explain the behaviour I'm getting from what I understand about ARC, so I'm hoping someone can explain what's going on here.

First off, in the following code I'm getting the behaviour I expect (please note that the NLog() output is given in the comment after the corresponding statement)

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[])
{
    NSObject *objPtr1 = [[NSObject alloc] init];
    NSObject *objPtr2 = objPtr1; 
    __weak NSObject *weakRef = objPtr1;
    NSLog(@"%@", [objPtr1 description]); // <NSObject: 0x1001107d0>
    objPtr1 = nil;
    NSLog(@"%@", [objPtr2 description]); // <NSObject: 0x1001107d0>
    objPtr2 = nil;
    NSLog(@"%@", [weakRef description]); // (null)
    return 0;
}

So in the above, right after weakRef is assigned, the NSObject instance has two strong pointers to it, and therefore a retain count of 2. After zeroing objPtr1 there's still one retaining pointer to the instance, so it's still in memory and responds to the description message. After nil-ing objPtr2, there are no strong pointers to the object and it is deallocated (I'm assuming it is, since weakRef has been zeroed). So far, so good.

Now, the same code with a small change:

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[])
{
    NSObject *objPtr1 = [[NSObject alloc] init];
    NSObject *objPtr2 = objPtr1; 
    __unsafe_unretained NSObject *weakRef = objPtr1; // __unsafe_unretained instead of just __weak
    NSLog(@"%@", [objPtr1 description]); // <NSObject: 0x1001107d0>

    objPtr1 = nil;
    NSLog(@"%@", [objPtr2 description]); // <NSObject: 0x1001107d0>

    objPtr2 = nil;
    NSLog(@"%@", [weakRef description]); // <NSObject: 0x1001107d0>
    //why was the object instance not deallocated and the preceding statement not crash the program?
    return 0;
}

I was expecting weakRef to become a dangling pointer sending a message through which would cause the program to crash in the third NSLog() statement, but it seems the object instance is still alive and well.

Another thing I find weird:

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[])
{

    NSObject *objPtr1 = [[NSObject alloc] init];
    NSObject *objPtr2 = objPtr1; 
    __weak NSObject *weakRef = objPtr1; // __weak again
    NSLog(@"%@", [weakRef description]); // <NSObject: 0x1001107d0>

    objPtr1 = nil;
    NSLog(@"%@", [weakRef description]); // <NSObject: 0x1001107d0>

    objPtr2 = nil;
    NSLog(@"%@", [weakRef description]); // <NSObject: 0x1001107d0>

    return 0;

}

This last code is like the first one (using the zeroed __weak pointer); the only difference is that the description message was sent to the object through weakRef in each of the three NSLog() calls. But this time round the object isn't deallocated even after the two strong references have been removed (since it's still responding to messages through weakRef).

So what's going on here?

jrturton
  • 118,105
  • 32
  • 252
  • 268
Aky
  • 1,777
  • 1
  • 14
  • 19
  • OK, it seems that in code #2 (with the unsafe unretained reference) if I insert an NSLog() statement (as a sort of filler) between "objPtr2 = nil" and "NSLog(@"%@", [weakRef description])" the program crashes sometimes (with an EXC_BAD_EXCESS error) and sometimes doesn't. If I insert another filler NSLog() statement, then the program definitely crashes (at least all the times I ran it). So I guess this means that reclaiming freed memory is not necessarily the absolute first thing that happens after an object loses all its strong references. – Aky Feb 07 '12 at 18:13
  • code #3 is still a puzzler for me, though... if I send a message to our instance via weakRef as in the code, then it doesn't seem to get deallocated even after all the strong references to it are removed - and not even when main() returns (I subclassed NSObject and overrode the dealloc method putting in an NSLog() to check whether it would be called - it wasn't). Seems like it might be a bug. – Aky Feb 07 '12 at 18:25
  • That does seem odd, yes. You're right about the code #2 - the memory just hasn't been re-used yet, so it still points to a "valid" `NSObject`, so `description` works fine. Code #3 is odd. I noticed it only doesn't zero `weakRef` if the 2nd `description` is sent to `weakRef`. i.e. if you send `description` to `weakRef` then `objPtr2` then `weakRef`, it does correctly output `(null)` for the last one. – mattjgalloway Feb 12 '12 at 00:26
  • Infact, you just have to call 2 methods on the object it would seem. Presumably some kind of bug here. I don't have time to hunt more but I'll take a further look some other time. An interesting one! – mattjgalloway Feb 12 '12 at 00:34
  • Just added an answer with a more cut down version of code 3 which I think shows it a bit better. I think it's a bug though, so we should file radars probably. – mattjgalloway Feb 12 '12 at 00:44
  • Did anyone get anywhere with working out what's going on here? – mattjgalloway Feb 21 '12 at 17:40

2 Answers2

3

If you disassemble A.R.C.-produced code, every access to a weak variable is wrapped in a call to this function:

id objc_loadWeak(id *location)
{
    return objc_autorelease(objc_loadWeakRetained(location));
}

This checks if the object has already been dealloced, and if not, retains and autoreleases it, for extra safety against premature deallocs.

Therefore in your third example, the early calls to method on weakRef are causing its retain count to be increased, so nilling your pointers doesn't cause it to be dealloced.

grahamparks
  • 16,130
  • 5
  • 49
  • 43
  • Indeed that's what happens, but it seems odd that the behaviour is different with the number of calls to methods on the object and even varies with the optimisation level. It seems like what we're doing is therefore undefined behaviour. But I can't really see why it's undefined looking at the documentation. Can you shed light on that with reference to documentation? – mattjgalloway Feb 12 '12 at 18:33
1

This does seem strange. You're right (in your comments) about the 2nd bit of code just being because the memory hasn't been re-used yet. But the 3rd bit of code is stranger. Here is a more simplified test case which shows this strange problem:

#import <Foundation/Foundation.h>

@interface SomeClass : NSObject 
@end

@implementation SomeClass
- (void)foo {
}
@end

int main (int argc, const char * argv[]) {
    @autoreleasepool {
        SomeClass *objPtr1 = [[SomeClass alloc] init];
        __weak SomeClass *weakRef = objPtr1;

//        [weakRef foo];
        [weakRef foo];

        objPtr1 = nil;

        NSLog(@"%p", weakRef);

        return 0;
    }
}

With that line commented out the output is:

$ clang -fobjc-arc -framework Foundation test.m -o test -O3 && ./test
2012-02-12 00:39:42.769 test[6684:707] 0x0

With that line uncommented out the output is:

$ clang -fobjc-arc -framework Foundation test.m -o test -O3 && ./test
2012-02-12 00:42:04.346 test[6688:707] 0x100f13f50

This seems deeply odd and looks entirely like a bug to me. I don't actually know what the answer is but thought I'd post this as an answer to get the ball rolling on figuring out what's going on.

Update:

If you build this at O0 then it seems that weakRef is zeroed only if there are no calls to foo. A single call to foo will mean that it won't get zeroed.

mattjgalloway
  • 34,792
  • 12
  • 100
  • 110
  • Remove the autorelease pool and it looks like objPtr1 is allocated as autoreleased when there is more than one call to it or its weak reference. This seems like an intentional optimization prevent even strong references from being added to the pool when it's only called once immediately after instantiation. – Jon Shier Feb 12 '12 at 00:56
  • In fact it looks like when compiled with -O0, weakRef isn't even zeroed, with or without an autorelease pool. – Jon Shier Feb 12 '12 at 01:06
  • @jshier I'm seeing that at `-O0`, `weakRef` is zeroed if I don't call `foo` any times but if I just call it once, then it's not zeroed. – mattjgalloway Feb 12 '12 at 11:51
  • I have a feeling that this - http://clang.llvm.org/docs/AutomaticReferenceCounting.html#ownership.semantics - is relevant here, but my brain is not letting me parse that information succinctly enough to get my head around it. Anyone else? – mattjgalloway Feb 12 '12 at 11:58