0

The code is as follows, Why the output of the following reference count is 1,2,3,4 respectively?

NSDictionary __weak *weak_dict;
@autoreleasepool {
    //生成并持有对象 alloc,new,copy,mutableCopy (注意以这些词开头的命名)
    NSDictionary *dict = [ [NSDictionary alloc] initWithObjects:@[@1, @2] forKeys:@[@"a", @"b"] ];
    NSDictionary  *dict1 = @{ @"1":@"c" };
    weak_dict = dict;
    NSLog(@"dict retain count : %ld", _objc_rootRetainCount(dict));  //1
    NSLog(@"dict retain count : %ld", _objc_rootRetainCount(weak_dict));  //2
    NSLog(@"%@ CFGetRetainCount %ld", weak_dict, CFGetRetainCount((__bridge CFTypeRef)weak_dict) );  //3
    NSLog(@"dict retain count : %ld", _objc_rootRetainCount(weak_dict));  //2
}
lkttle
  • 19
  • 2
  • “Why the output of the following reference count is 1,2,3,4 respectively?” … Where is the 4? Your code comment suggests the last line returned 2. – Rob Aug 15 '21 at 16:34
  • Did you mean to include `dict1` in this? You don’t appear to use it… – Rob Aug 15 '21 at 16:44
  • 1
    The [Advanced Memory Management Programming Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011-SW1) advises, “Thinking about memory management from the perspective of reference counting, however, is frequently counterproductive, because you tend to consider memory management in terms of the implementation details rather than in terms of your actual goals. Instead, you should think of memory management from the perspective of object ownership and object graphs.” – Rob Aug 15 '21 at 16:44

1 Answers1

1

First, it's critical that your realize that retainCount is useless. Using an internal function like _objc_rootRetainCount doesn't change that. The values output are completely compiler-dependent and can change at any time due to optimization or framework changes. While there's a certain benefit to digging into the implementation details, chasing the actual value of retain counts more often causes developers to go astray rather than illuminate anything. (But do read the comments between Alexander and me below; this is all true, but also misleading. There are reasons to look at retain counts.)

That said, it is not magic, and is totally knowable. There just isn't any specific rule. You have to look at exactly what the optimizer has done. All discussion here is for Xcode 13.0 Beta 5 in Debug, built for Mac ARM. Other versions or configurations could return different values because some of the retains here could be removed if the optimizer were smarter (and it might be in the future). You can explore this all by looking at the Assembly output, or by using the Hopper decompiler (which is what I did).

NSLog(@"dict retain count : %ld", _objc_rootRetainCount(dict));  //1

No surprises here I hope. _objc_rootRetainCount prints the retain count and that's 1 because you called alloc to create it. There's no promise that this be 1, though. It could be any positive value. The only thing you're actually promised that you are responsible for sending exactly one release, and if you do that, everything will balance.

NSLog(@"dict retain count : %ld", _objc_rootRetainCount(weak_dict));  //2

Since weak_dict is weak, it needs to first create a strong reference before passing to _objc_rootRetainCount (generally the caller for a function is responsible for ensuring that the arguments it passes continue to exist for the lifetime of the call). This effectively behaves like:

id strong_weak_dict = [weak_dict retain];
NSLog(@"dict retain count : %ld", _objc_rootRetainCount(strong_weak_dict));
[strong_weak_dict release];

A smarter future optimizer might be able to figure out that the existing dict reference is equivalent and avoid this extra retain/release. There is no promise that it happens. The only requirement is that the caller must ensure the argument's lifetime extends beyond the call to _objc_rootRetainCount. It can do that any way it likes.

NSLog(@"%@ CFGetRetainCount %ld", weak_dict, CFGetRetainCount((__bridge CFTypeRef)weak_dict) );  //3

Same thing here, there are just two copies of weak_dict passed to two different functions (NSLog and CFGetRetainCount). The compiler inserts two retains and two releases:

id strong_weak_dict = [weak_dict retain];
[strong_weak_dict retain];
NSLog(@"%@ CFGetRetainCount %ld", strong_weak_dict, CFGetRetainCount((__bridge CFTypeRef) strong_weak_dict) );
[strong_weak_dict release];
[strong_weak_dict release];

It could be smarter and get rid of one of those (or it could even get rid of both if it realized that dict is doing the job already).

NSLog(@"dict retain count : %ld", _objc_rootRetainCount(weak_dict));  //2

And as above, passing weak_dict wraps a retain/release, so you get 2.

One slightly odd thing in the output from 13.0 Beta 5 is that dict1 has an extra retain with no corresponding release. I'm not sure why that happens. (This is fine since retain and release are no-ops for constants, I just don't understand why the call to retain is inserted.)

There are several details I glossed over here to make the behavior a bit simpler. The compiler doesn't call retain, it calls objc_loadWeakRetained. The specific way it handles the "double retain" includes an extra temporary variable. And there's a call to objc_storeWeak that I didn't discuss, but also doesn't directly impact your question.

There is a performance lesson in here, which is that unnecessary use of weak can insert a lot of extra retains and releases, which are not free. Generally you should convert weak pointers to strong pointers one time in a function and then use the strong pointer. This also ensures that the object survives the entire function, which is better for reasoning about it. Weak pointers can become nil between one statement and the next. So this is a good question and worth exploring; you just shouldn't rely on any of these details.

Here's the full pseudo-C decompiled output:

int _main(int arg0, int arg1) {
    var_48 = objc_autoreleasePoolPush();
    var_20 = [objc_alloc() initWithObjects:r2 forKeys:0x1000080f8];
    [0x100008110 retain];
    objc_storeWeak(&saved_fp - 0x18, var_20);
    stack[-192] = _objc_rootRetainCount();
    NSLog(@"dict retain count : %ld", @selector(initWithObjects:forKeys:));
    var_68 = objc_loadWeakRetained(&saved_fp - 0x18);
    stack[-192] = _objc_rootRetainCount();
    NSLog(@"dict retain count : %ld", @selector(initWithObjects:forKeys:));
    [var_68 release];
    var_80 = objc_loadWeakRetained(&saved_fp - 0x18);
    r0 = objc_loadWeakRetained(&saved_fp - 0x18);
    var_78 = r0;
    r0 = CFGetRetainCount(r0);
    stack[-192] = var_80;
    *(&stack[-192] + 0x8) = r0;
    NSLog(@"%@ CFGetRetainCount %ld", @selector(initWithObjects:forKeys:), 0x1000080e0);
    [var_78 release];
    [var_80 release];
    var_98 = objc_loadWeakRetained(&saved_fp - 0x18);
    stack[-192] = _objc_rootRetainCount();
    NSLog(@"dict retain count : %ld", @selector(initWithObjects:forKeys:));
    [var_98 release];
    objc_storeStrong(&saved_fp - 0x38, 0x0);
    objc_storeStrong(&saved_fp - 0x20, 0x0);
    objc_autoreleasePoolPop(var_48);
    objc_destroyWeak(&saved_fp - 0x18);
    return 0x0;
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Great answer overall, but I've got a comment about the notion that check RC is useless. I've seen a lot of people say that , but I don't see many people considering how/why OP got the idea to check the retain count in the first place. Reference counting, just like conventional mark-and-sweep GC, is a leaky abstraction. None of the implementation details matter and everything "just works" and is magic and great, until it's not, and all of a sudden implementation details offer insight into exactly what might be going wrong. I.e. how would you debug a memory leak not caught by instruments? – Alexander Aug 15 '21 at 17:37
  • E.g. I've used checked the RCs in the past to debug a mixup between `Unmanaged.takeRetainedValue()` and `Unmanaged.takeUnretainedValue()`. What should I have used instead? – Alexander Aug 15 '21 at 17:37
  • To debug memory leaks, you should check **deltas** in RC, not the actual values. And as I note above, it is worth understanding how it works; the numbers themselves, however, typically will take you down the wrong roads when debugging. You can only really compare the values to other values that you see in code that "works." I've run into Foundation objects that surprise me by having a persistent RC of 3 (even after autoreleasepool drain). NSNumber has particularly surprised me (and changed over the years). – Rob Napier Aug 15 '21 at 17:43
  • Yes, you're right, and checking deltas was exactly what I was doing. But then the statement "retainCount" is useless isn't true, it's "checking absolute retainCounts is useless". As for NSNumber, yeah, that doesn't surprise me, I think all tagged pointers had "invalid" retain counts – Alexander Aug 15 '21 at 17:46
  • I think it's a fair criticism that every experienced Cocoa dev says and does the same thing: We all say to junior devs "don't look at retain count because you don't understand it and it will mess you up." We all look at retain counts occasionally ourselves because we do understand it (and it still messes us up). And we all assume that once someone becomes experienced enough, they'll just ignore us anyway, and also understand why we say it. (But agreed that probably is not a good way to mentor… retain counts are confusing, but they're not magic. It's just code.) – Rob Napier Aug 15 '21 at 18:05
  • 1
    "But agreed that probably is not a good way to mentor… retain counts are confusing, but they're not magic. It's just code." my thoughts exactly. And yeah, there is this element of "once you've learned the rules of the trade and get experienced enough, you learn when/how to bend the rules", but I don't think this is a good place for it, because it just injects some confusion and self doubt. E.g. I thought there was some legitimately better debugging tool that fully alleviates all need to worry about RCs lol – Alexander Aug 15 '21 at 18:07