Inconsistent object deallocation with ARC?

2019-03-16 00:52发布

问题:

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?

回答1:

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.



回答2:

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.