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;
}