环境:Xcode11
ARC 在MRC时代 id obj1 = obj;
这一行代码仅仅是赋值,不会引起obj对象引用计数的任何变化。而在ARC时代这一行代码会导致对象的引用计数+1。这背后究竟发生了什么?接下来让我们研究一下。
ARC:自动引用计数。不再需要手动调用 retain 、release、autorelease 等方法,而是由编译器在合适的地方自动添加。那么编译器又是怎么知道是添加retain还是添加autorelease或者weak的呢?这就需要所有权修饰符了。通过不同类型的所有权修饰符,编译器就知道该添加什么语句了。
ARC 有效时,id 类型和对象类型 必须附加所有权修饰符,有如下几种:
__strong 修饰符
__weak 修饰符
__unsafe_unretained 修饰符
__autoreleasing 修饰符
其中 __strong
修饰符是OC对象类型的默认修饰符。
接下来从汇编角度看,当一个指针变量使用这些修饰符后,编译器会作何处理。
__strong 修饰符 对于下面的代码:
1 2 3 4 5 6 7 8 9 10 int main (int argc, const char * argv[]) { @autoreleasepool { id obj = [NSObject new]; { id __strong obj1 = obj; NSLog(@"obj1:%@" , obj1); } } return 0 ; }
对应汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Ltmp0: .loc 1 12 22 prologue_end ## WeakDemo/main.m:12:22 callq _objc_autoreleasePoolPush Ltmp1: .loc 1 13 18 ## WeakDemo/main.m:13:18 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rcx, %rdi movq %rax, -40(%rbp) ## 8-byte Spill callq *_objc_msgSend@GOTPCREL(%rip) .loc 1 13 12 is_stmt 0 ## WeakDemo/main.m:13:12 movq %rax, -24(%rbp) Ltmp2: .loc 1 15 23 is_stmt 1 ## WeakDemo/main.m:15:23 movq -24(%rbp), %rdi callq *_objc_retain@GOTPCREL(%rip) leaq L__unnamed_cfstring_(%rip), %rcx movq %rax, -32(%rbp) .loc 1 16 29 ## WeakDemo/main.m:16:29 movq -32(%rbp), %rsi .loc 1 16 11 is_stmt 0 ## WeakDemo/main.m:16:11 movq %rcx, %rdi movb $0, %al callq _NSLog xorl %edx, %edx movl %edx, %esi Ltmp3: .loc 1 17 9 is_stmt 1 ## WeakDemo/main.m:17:9 leaq -32(%rbp), %rdi callq _objc_storeStrong xorl %edx, %edx movl %edx, %esi .loc 1 18 5 ## WeakDemo/main.m:18:5 leaq -24(%rbp), %rdi callq _objc_storeStrong movq -40(%rbp), %rdi ## 8-byte Reload callq _objc_autoreleasePoolPop xorl %eax, %eax
上述汇编对应的编译器的模拟源代码:
1 2 3 4 5 6 7 id obj = objc_msgSend(NSObject, "new" ); id obj1 = objc_retain(obj); NSLog(@"obj1:%@" , obj1); objc_storeStrong(&obj1, nil); objc_storeStrong(&obj, nil);
id __strong obj1 = obj;
这句代码实际上被编译器转换为:id obj1 = objc_retain(obj);
。
看一下函数objc_retain的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __attribute__((aligned (16 ), flatten, noinline)) id objc_retain (id obj) { if (!obj) return obj; if (obj->isTaggedPointer ()) return obj; return obj->retain (); } inline id objc_object::retain () { ASSERT (!isTaggedPointer ()); if (fastpath (!ISA ()->hasCustomRR ())) { return rootRetain (); } return ((id (*)(objc_object *, SEL))objc_msgSend)(this , @selector (retain)); }
抛开那些判断,objc_retain函数最终调用了 retain 方法。对象的引用计数加 1 。这也就是我们常说的在 ARC 下将一个对象赋值给一个强引用的指针变量时,编译器会自动插入一条 retain 方法。
接着往下看,当obj1作用域结束后会执行一句objc_storeStrong(&obj1, nil);
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void objc_storeStrong (id *location, id obj) { id prev = *location; if (obj == prev) { return ; } objc_retain (obj); *location = obj; objc_release (prev); } __attribute__((aligned (16 ), flatten, noinline)) void objc_release (id obj) { if (!obj) return ; if (obj->isTaggedPointer ()) return ; return obj->release (); } inline void objc_object::release () { ASSERT (!isTaggedPointer ()); if (fastpath (!ISA ()->hasCustomRR ())) { rootRelease (); return ; } ((void (*)(objc_object *, SEL))objc_msgSend)(this , @selector (release)); }
objc_storeStrong函数的作用:
retain新对象,将指针变量指向新对象,release旧对象。特别的,如果 obj 等于 nil ,上述逻辑代表着将指针变量赋值为 nil,并且 release之前指向的对象。因此 objc_storeStrong(&obj1, nil);
代表着 release obj1 指向的对象,并将obj1置为nil。
这也就是我们常说的在 ARC 下一个强引用的指针变量超出作用域被废弃时,编译器会自动插入一条 release 方法,另外其实还把强引用的指针变量置为nil了。
例子2:
将一个使用new方法创建的对象赋值给一个 __strong
修饰的指针变量
1 2 3 4 5 6 7 8 9 int main (int argc, const char * argv[]) { @autoreleasepool { { id __strong obj1 = [NSObject new]; NSLog(@"obj1:%@" , obj1); } } return 0 ; }
汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Ltmp0: .loc 1 30 22 prologue_end ## WeakDemo/main.m:30:22 callq _objc_autoreleasePoolPush Ltmp1: .loc 1 32 30 ## WeakDemo/main.m:32:30 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rcx, %rdi movq %rax, -32(%rbp) ## 8-byte Spill callq *_objc_msgSend@GOTPCREL(%rip) leaq L__unnamed_cfstring_(%rip), %rcx .loc 1 32 23 is_stmt 0 ## WeakDemo/main.m:32:23 movq %rax, -24(%rbp) .loc 1 33 29 is_stmt 1 ## WeakDemo/main.m:33:29 movq -24(%rbp), %rsi .loc 1 33 11 is_stmt 0 ## WeakDemo/main.m:33:11 movq %rcx, %rdi movb $0, %al callq _NSLog xorl %edx, %edx movl %edx, %esi Ltmp2: .loc 1 34 9 is_stmt 1 ## WeakDemo/main.m:34:9 leaq -24(%rbp), %rdi callq _objc_storeStrong movq -32(%rbp), %rdi ## 8-byte Reload .loc 1 35 5 ## WeakDemo/main.m:35:5 callq _objc_autoreleasePoolPop xorl %eax, %eax
等价于:
1 2 3 id obj1 = objc_msgSend(NSObject, "new" ); NSLog(@"obj1:%@" , obj1); objc_storeStrong(&obj1, nil);
编译器发现obj1可以持有对象,又是创建对象,于是不再 retain。
例子 3:
将一个使用myPerson方法创建的对象赋值给一个 __strong
修饰的指针变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main(int argc, const char * argv[]) { @autoreleasepool { { id __strong obj1 = [ARCPerson myPerson]; NSLog (@"obj1:%@" , obj1); } } return 0 ; } + (instancetype )myPerson { ARCPerson *p = [ARCPerson new]; return p; }
汇编:
上述汇编对应的编译器的模拟源代码:
1 2 3 4 5 6 id obj1 id tmp = objc_msgSend(ARCPerson , "myPerson" ) tmp = objc_autoreleaseReturnValue(tmp ) obj1 = objc_retainAutoreleasedReturnValue(tmp ) NSLog(@"obj1:%@" , obj1) objc_storeStrong(&obj1 , nil )
objc_autoreleaseReturnValue实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 id objc_autoreleaseReturnValue (id obj) { if (prepareOptimizedReturn(ReturnAtPlus1)) return obj; return objc_autorelease(obj); } static ALWAYS_INLINE bool prepareOptimizedReturn (ReturnDisposition disposition) { ASSERT(getReturnDisposition() == ReturnAtPlus0); if (callerAcceptsOptimizedReturn(__builtin_return_address(0 ))) { if (disposition) setReturnDisposition(disposition); return true ; } return false ; } static ALWAYS_INLINE ReturnDisposition acceptOptimizedReturn () { ReturnDisposition disposition = getReturnDisposition(); setReturnDisposition(ReturnAtPlus0); return disposition; } static ALWAYS_INLINE ReturnDisposition getReturnDisposition () { return (ReturnDisposition)(uintptr_t )tls_get_direct(RETURN_DISPOSITION_KEY); } static ALWAYS_INLINE void setReturnDisposition (ReturnDisposition disposition) { tls_set_direct(RETURN_DISPOSITION_KEY, (void *)(uintptr_t )disposition); }
objc_retainAutoreleasedReturnValue实现:
1 2 3 4 5 6 7 id objc_retainAutoreleasedReturnValue (id obj) { if (acceptOptimizedReturn() == ReturnAtPlus1) return obj; return objc_retain(obj); }
objc_autoreleaseReturnValue会做一个优化,如果检测到后续紧接着调用了objc_retainAutoreleasedReturnValue,则往 TLS 写ReturnAtPlus1标识,表明返回的对象的内存管理有优化。此时objc_autoreleaseReturnValue不会再将生成的对象真的注册到自动释放池中而是直接返回。当执行objc_retainAutoreleasedReturnValue时,会先从TLS中取值检测是否等于ReturnAtPlus1,相等说明走的是优化路径则不 retain,避免重复retain。
因此以其他单词命名开头的方法创建并返回的对象,该对象本应该注册到自动释放池,但如果创建后马上就赋值给一个 __strong
修饰符的指针变量,此时系统会做一个优化,该对象将不再注册到自动释放池中,因此能够及时释放对象,起到优化作用。 note:使用系统的对象 id __strong obj1 = [NSArray array];
可能看不到这种优化。
总结:将一个对象赋值给一个 __strong
修饰符的指针变量,编译器会视情况插入objc_retain
或objc_retainAutoreleasedReturnValue
函数的调用。而当指针变量超出作用域后被销毁时,会插入objc_storeStrong(&obj, nil);
释放对象,并置为nil。
objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue是一对优化。
objc_retainAutoreleaseReturnValue 和 objc_unsafeClaimAutoreleasedReturnValue 是另一对优化。
如果优化成功,他们都避免了多余的注册到自动释放池的过程,加快了对象的释放。
__weak 修饰符 对于下面的代码:
1 2 3 4 5 6 7 8 9 10 int main (int argc, const char * argv[]) { @autoreleasepool { id obj = [NSObject new]; { id __weak obj1 = obj; NSLog(@"obj1:%@" , obj1); } } return 0 ; }
对应的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 Ltmp3: .loc 1 12 22 prologue_end ## WeakDemo/main.m:12:22 callq _objc_autoreleasePoolPush Ltmp4: .loc 1 13 18 ## WeakDemo/main.m:13:18 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq _objc_msgSend@GOTPCREL(%rip), %rcx movq %rax, -56(%rbp) ## 8-byte Spill callq *%rcx .loc 1 13 12 is_stmt 0 ## WeakDemo/main.m:13:12 movq %rax, -24(%rbp) Ltmp5: .loc 1 15 28 is_stmt 1 ## WeakDemo/main.m:15:28 movq -24(%rbp), %rsi leaq -32(%rbp), %rax .loc 1 15 21 is_stmt 0 ## WeakDemo/main.m:15:21 movq %rax, %rdi movq %rax, -64(%rbp) ## 8-byte Spill callq _objc_initWeak movq -64(%rbp), %rdi ## 8-byte Reload movq %rax, -72(%rbp) ## 8-byte Spill .loc 1 16 29 is_stmt 1 ## WeakDemo/main.m:16:29 callq _objc_loadWeakRetained movq %rax, %rcx Ltmp0: .loc 1 16 11 is_stmt 0 ## WeakDemo/main.m:16:11 leaq L__unnamed_cfstring_(%rip), %rdi xorl %edx, %edx ## kill: def $dl killed $dl killed $edx movq %rax, %rsi movb %dl, %al movq %rcx, -80(%rbp) ## 8-byte Spill callq _NSLog Ltmp1: jmp LBB0_1 LBB0_1: .loc 1 0 11 ## WeakDemo/main.m:0:11 movq -80(%rbp), %rdi ## 8-byte Reload .loc 1 16 11 ## WeakDemo/main.m:16:11 callq *_objc_release@GOTPCREL(%rip) Ltmp6: .loc 1 17 9 is_stmt 1 ## WeakDemo/main.m:17:9 leaq -32(%rbp), %rdi callq _objc_destroyWeak xorl %eax, %eax movl %eax, %esi .loc 1 18 5 ## WeakDemo/main.m:18:5 leaq -24(%rbp), %rdi callq _objc_storeStrong movq -56(%rbp), %rdi ## 8-byte Reload callq _objc_autoreleasePoolPop xorl %eax, %eax
上述汇编对应的编译器的模拟源代码:
1 2 3 4 5 6 7 8 9 id obj = objc_msgSend(NSObject , "new" );id obj1 = objc_initWeak(&obj1, obj);id tmp = objc_loadWeakRetained(&obj1);NSLog (@"obj1:%@" , tmp);objc_release(tmp); objc_destroyWeak(&obj1); objc_storeStrong(&obj, nil );
可以看到 id __weak obj1 = obj;
,这句代码实际上被编译器转换为:id obj1 = objc_initWeak(&obj1, obj);
。没有调用什么 retain 方法,这就是使用__weak修饰符不会导致对象引用计数加 1 的原因。
but,objc_initWeak函数又是干什么的,这里不做详述,有兴趣的可以查看 weak 的实现原理。这里简单说下objc_initWeak的作用就是将一个指针变量的地址登记到对象的 weak 表中。它的实现是:
1 2 3 4 5 6 7 8 9 10 11 id objc_initWeak (id *location, id newObj) { if (!newObj) { *location = nil; return nil; } return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj); }
接着往下看,会发现 NSLog(@"obj1:%@", obj1);
代码变成了三句:
1 2 3 id tmp = objc_loadWeakRetained(&obj1); NSLog(@"obj1:%@" , tmp); objc_release(tmp);
如果对象还可用时执行了id tmp = objc_loadWeakRetained(&obj1);
,那么即使对象在其他线程被释放了,对象此刻也不会被销毁,能够保证NSLog(@"obj1:%@", obj1);
方法在执行完前对象都是可用的。
以前这里是将对象注册到自动释放池的,可以参考:Why __weak object will be added to autorelease pool? 。经过测试发现在MRC中还是将对象注册到自动释放池的。这时代码等价于:
1 2 3 id tmp = objc_loadWeakRetained(&obj1); objc_autorelease(tmp); NSLog(@"obj1:%@" , tmp);
举个例子:
1 2 3 4 5 6 7 8 9 10 11 - (void )hello { { MRCCat *cat = [MRCCat new]; self .cat = cat; NSLog (@"%@ 1" , self .cat); [cat release]; } NSLog (@"%@ 2" , self .cat); [self .cat hello]; NSLog (@"%@ hello" , self ); }
我们会发现在调用 [cat release];
后,cat对象并没有马上销毁。
我们可以在NSLog(@"%@ 1", self.cat);
这一行打上断点,并执行 po _objc_autoreleasePoolPrint()
:
1 2 3 4 5 6 7 8 objc: 0x7fcd6ef0a5c0 UIWindow objc: 0x60000032c6c0 __NSArrayM objc: 0x7fcd6ef09f20 ViewController objc: 0x7fcd6ef0a5c0 UIWindow objc: 0x60000032c8a0 __NSArrayM objc: 0x7fcd6ef09f20 ViewController objc: 0x7fcd6ef09f20 ViewController objc: ##############
此时AUTORELEASE POOLS里还没有MRCCat对象,接着在 [cat release];
打上断点,再次执行 po _objc_autoreleasePoolPrint()
:
1 2 3 4 5 6 objc[3377 ]: [0x7fcd70848248 ] 0x7fcd6ef0a5c0 UIWindow objc[3377 ]: [0x7fcd70848250 ] 0x60000032c8a0 __NSArrayM objc[3377 ]: [0x7fcd70848258 ] 0x7fcd6ef09f20 ViewController objc[3377 ]: [0x7fcd70848260 ] 0x7fcd6ef09f20 ViewController objc[3377 ]: [0x7fcd70848268 ] 0x600000f747b0 MRCCat objc[3377 ]: ##############
可以看到MRCCat被注册到自动释放池里去了。所以后面即使调用 [cat release];
cat对象也不会马上销毁。如果你在[self.cat hello];
打上断点,再次执行 po _objc_autoreleasePoolPrint()
:
1 2 3 4 5 6 7 8 objc: 0x7fcd6ef09f20 ViewController objc: 0x7fcd6ef0a5c0 UIWindow objc: 0x60000032c8a0 __NSArrayM objc: 0x7fcd6ef09f20 ViewController objc: 0x7fcd6ef09f20 ViewController objc: 0x600000f747b0 MRCCat objc: 0x600000f747b0 MRCCat objc: ##############
你会发现cat对象被注册了两次。
因此使用附有 __weak
修饰符的变量,等价于使用retain并注册到autoreleasepool中的对象。如果你某段代码需要多次使用该weak变量,最好先retain后赋值给一个变量然后使用该变量。要不然每次都会retain+注册到自动释放池。所幸的是现在几乎接触不到MRC的代码,并且在ARC中使用附有 __weak
修饰符的变量,系统也不会retain后再将其注册到autoreleasepool中了,而是在使用前调用retain,使用后调用release马上释放。
另外我们对上述代码稍作改动,添加一个@autoreleasepool:
1 2 3 4 5 6 7 8 9 10 11 - (void )hello { @autoreleasepool { MRCCat *cat = [MRCCat new]; self .cat = cat; NSLog (@"%@ 1" , self .cat); [cat release]; } NSLog (@"%@ 2" , self .cat); [self .cat hello]; NSLog (@"%@ hello" , self ); }
就会发现cat对象在autoreleasepool块执行完成后马上就被销毁了。
继续,当作用域结束时,obj1 变量被废弃,因此编译器插入了一条objc_destroyWeak函数的调用。该方法会将 obj1弱引用指针变量从对象的 weak 表中移除(这里obj1变量先于obj对象废弃,因此obj1不会被置为nil)。
objc_destroyWeak 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 void objc_destroyWeak (id *location) { (void )storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating> (location, nil); } static id storeWeak (id *location, objc_object *newObj) { ... if (haveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } ... return (id)newObj; } void weak_unregister_no_lock (weak_table_t *weak_table, id referent_id, id *referrer_id) { objc_object *referent = (objc_object *)referent_id; objc_object **referrer = (objc_object **)referrer_id; weak_entry_t *entry; if (!referent) return ; if ((entry = weak_entry_for_referent(weak_table, referent))) { remove_referrer(entry, referrer); bool empty = true ; if (entry->out_of_line() && entry->num_refs != 0 ) { empty = false ; } else { for (size_t i = 0 ; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i]) { empty = false ; break ; } } } if (empty) { weak_entry_remove(weak_table, entry); } } } static void remove_referrer (weak_entry_t *entry, objc_object **old_referrer) { if (! entry->out_of_line()) { for (size_t i = 0 ; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i] == old_referrer) { entry->inline_referrers[i] = nil; return ; } } _objc_inform("Attempted to unregister unknown __weak variable " "at %p. This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n" , old_referrer); objc_weak_error(); return ; } size_t begin = w_hash_pointer(old_referrer) & (entry->mask); size_t index = begin; size_t hash_displacement = 0 ; while (entry->referrers[index] != old_referrer) { index = (index+1 ) & entry->mask; if (index == begin) bad_weak_table(entry); hash_displacement++; if (hash_displacement > entry->max_hash_displacement) { _objc_inform("Attempted to unregister unknown __weak variable " "at %p. This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n" , old_referrer); objc_weak_error(); return ; } } entry->referrers[index] = nil; entry->num_refs--; }
weak_unregister_no_lock
的作用:将弱引用指针变量从对象的weak表里移除。如果weak表中对象的weak_entry为空,则将entry也移除。
上述就是使用 __weak
修饰符修饰指针变量时,ARC 对其的处理。
再来看一个例子:
将new方法刚创建的对象赋值给一个__weak指针变量
1 2 3 4 5 6 7 8 9 int main (int argc, const char * argv[]) { @autoreleasepool { { id __weak obj1 = [NSObject new]; NSLog(@"obj1:%@" , obj1); } } return 0 ; }
你可能知道打印的结果为 null,但它对应的汇编又是怎样的呢?
对应的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 Ltmp3: .loc 1 30 22 prologue_end ## WeakDemo/main.m:30:22 callq _objc_autoreleasePoolPush Ltmp4: .loc 1 32 28 ## WeakDemo/main.m:32:28 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq _objc_msgSend@GOTPCREL(%rip), %rcx movq %rax, -48(%rbp) ## 8-byte Spill callq *%rcx leaq -24(%rbp), %rcx .loc 1 32 21 is_stmt 0 ## WeakDemo/main.m:32:21 movq %rcx, %rdi movq %rax, %rsi movq %rax, -56(%rbp) ## 8-byte Spill movq %rcx, -64(%rbp) ## 8-byte Spill callq _objc_initWeak movq _objc_release@GOTPCREL(%rip), %rcx movq -56(%rbp), %rdi ## 8-byte Reload movq %rax, -72(%rbp) ## 8-byte Spill callq *%rcx movq -64(%rbp), %rdi ## 8-byte Reload .loc 1 33 29 is_stmt 1 ## WeakDemo/main.m:33:29 callq _objc_loadWeakRetained movq %rax, %rcx Ltmp0: .loc 1 33 11 is_stmt 0 ## WeakDemo/main.m:33:11 leaq L__unnamed_cfstring_(%rip), %rdi xorl %edx, %edx ## kill: def $dl killed $dl killed $edx movq %rax, %rsi movb %dl, %al movq %rcx, -80(%rbp) ## 8-byte Spill callq _NSLog Ltmp1: jmp LBB0_1 LBB0_1: .loc 1 0 11 ## WeakDemo/main.m:0:11 movq -80(%rbp), %rdi ## 8-byte Reload .loc 1 33 11 ## WeakDemo/main.m:33:11 callq *_objc_release@GOTPCREL(%rip) Ltmp5: .loc 1 34 9 is_stmt 1 ## WeakDemo/main.m:34:9 leaq -24(%rbp), %rdi callq _objc_destroyWeak movq -48(%rbp), %rdi ## 8-byte Reload .loc 1 35 5 ## WeakDemo/main.m:35:5 callq _objc_autoreleasePoolPop xorl %eax, %eax
上述汇编对应的编译器的模拟源代码:
1 2 3 4 5 6 7 id tmp = objc_msgSend(NSObject , "new" ) id obj1 = objc_initWeak(&obj1 , tmp) objc_release(tmp ) id tmp1 = objc_loadWeakRetained(&obj1 ) NSLog(@"obj1:%@" , tmp1) objc_release(tmp1 ) objc_destroyWeak(&obj1 )
可以看到objc_initWeak后,马上调用了objc_release,导致对象被释放销毁,同时 obj1 会被置为 nil,于是后面的 tmp1 也是 nil。打印也是 nil。
对于代码id __weak obj1 = [NSObject new];
,由于__weak 修饰的指针变量无法持有对象,因此对象一创建就被销毁。
另外一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 int main(int argc, const char * argv[]) { @autoreleasepool { id obj = [NSObject new]; id __weak ref = obj; NSLog (@"ref-1:%@" , ref); { id obj2 = [NSObject new]; ref = obj2; NSLog (@"ref-2:%@" , ref); } } return 0 ; }
总结
__weak
修饰符作用:
1.当我们将一个对象赋值给一个 __weak
修饰符的指针变量时,编译器会插入 objc_initWeak
或 objc_storeWeak
函数将该弱指针变量从旧对象的weak表剔除(如果之前有指向对象的话)并注册到新对象的weak表里,然后将弱指针指向新对象。
2.当使用附有 __weak
修饰符的变量时,编译器会在使用前插入 objc_loadWeakRetained
函数将对象引用计数+1,保证对象在后面的使用过程中不被释放。并在使用后插入 objc_release(tmp1);
释放对象。
3.当弱指针变量超出作用域被废弃时,编译器会插入 objc_destroyWeak
将该变量从对象的weak表里剔除。
4.当对象销毁时,对象的weak表中的所有弱指针变量会被赋值为nil,避免了野指针崩溃,比如对nil对象发送消息不会有任何反应,但是如果是对nil进行解引用还是会崩溃的。
这些功能是assign所不具备的,assign唯一与weak相同的地方就是不会导致对象的引用计数+1。
__autoreleasing修饰符 对于代码:
1 2 3 4 5 6 7 8 9 10 int main(int argc, const char * argv[]) { @autoreleasepool { id obj = [NSObject new]; { id __autoreleasing obj1 = obj; NSLog (@"obj1:%@" , obj1); } } return 0 ; }
对应汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Ltmp0: .loc 1 30 22 prologue_end ## WeakDemo/main.m:30:22 callq _objc_autoreleasePoolPush Ltmp1: .loc 1 31 18 ## WeakDemo/main.m:31:18 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rcx, %rdi movq %rax, -40(%rbp) ## 8-byte Spill callq *_objc_msgSend@GOTPCREL(%rip) .loc 1 31 12 is_stmt 0 ## WeakDemo/main.m:31:12 movq %rax, -24(%rbp) Ltmp2: .loc 1 33 30 is_stmt 1 ## WeakDemo/main.m:33:30 movq -24(%rbp), %rdi callq _objc_retainAutorelease leaq L__unnamed_cfstring_(%rip), %rcx movq %rax, -32(%rbp) .loc 1 34 29 ## WeakDemo/main.m:34:29 movq -32(%rbp), %rsi .loc 1 34 11 is_stmt 0 ## WeakDemo/main.m:34:11 movq %rcx, %rdi movb $0, %al callq _NSLog xorl %edx, %edx movl %edx, %esi Ltmp3: .loc 1 36 5 is_stmt 1 ## WeakDemo/main.m:36:5 leaq -24(%rbp), %rdi callq _objc_storeStrong movq -40(%rbp), %rdi ## 8-byte Reload callq _objc_autoreleasePoolPop xorl %eax, %eax
模拟代码:
1 2 3 4 id obj = msg_Send(NSObject, "new" ); id obj1 = objc_retainAutorelease(obj); NSLog(@"obj1:%@" , obj1); objc_storeStrong(&obj, nil);
objc_retainAutorelease实现:
1 2 3 4 5 id objc_retainAutorelease (id obj ){ return objc_autorelease (objc_retain (obj )); }
会先 retain 对象,再将对象注册到自动释放池里。
所以id __autoreleasing obj1 = obj;
执行完后对象的引用计数其实是 2。
例子 2:
创建一个对象并赋值给__autoreleasing 的obj1
1 2 3 4 5 6 7 8 9 int main (int argc, const char * argv[]) { @autoreleasepool { { id __autoreleasing obj1 = [NSObject new ]; NSLog(@"obj1:%@" , obj1); } } return 0 ; }
汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Ltmp0: .loc 1 30 22 prologue_end ## WeakDemo/main.m:30:22 callq _objc_autoreleasePoolPush Ltmp1: .loc 1 32 37 ## WeakDemo/main.m:32:37 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rcx, %rdi movq %rax, -32(%rbp) ## 8-byte Spill callq *_objc_msgSend@GOTPCREL(%rip) .loc 1 32 30 is_stmt 0 ## WeakDemo/main.m:32:30 movq %rax, %rdi callq _objc_autorelease leaq L__unnamed_cfstring_(%rip), %rcx movq %rax, -24(%rbp) .loc 1 33 29 is_stmt 1 ## WeakDemo/main.m:33:29 movq -24(%rbp), %rsi .loc 1 33 11 is_stmt 0 ## WeakDemo/main.m:33:11 movq %rcx, %rdi movb $0, %al callq _NSLog movq -32(%rbp), %rdi ## 8-byte Reload Ltmp2: .loc 1 35 5 is_stmt 1 ## WeakDemo/main.m:35:5 callq _objc_autoreleasePoolPop xorl %eax, %eax
模拟代码:
1 2 3 id obj1 = msg_Send(NSObject , "new" ) objc_autorelease(obj1 ) NSLog(@"obj1:%@" , obj1)
objc_autorelease实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __attribute__((aligned (16 ), flatten, noinline)) id objc_autorelease (id obj) { if (!obj) return obj; if (obj->isTaggedPointer ()) return obj; return obj->autorelease (); } inline id objc_object::autorelease () { ASSERT (!isTaggedPointer ()); if (fastpath (!ISA ()->hasCustomRR ())) { return rootAutorelease (); } return ((id (*)(objc_object *, SEL))objc_msgSend)(this , @selector (autorelease)); }
如果是创建一个对象并赋值给__autoreleasing 的指针变量,则仅仅是将对象注册到自动释放池。
因此id __autoreleasing obj1 = [NSObject new];
执行完成后,对象的引用计数为 1。
__unsafe_unretained 修饰符 代码:
1 2 3 4 5 6 7 8 9 10 int main(int argc, const char * argv[]) { @autoreleasepool { id obj = [NSObject new]; { id __unsafe_unretained obj1 = obj; NSLog (@"obj1:%@" , obj1); } } return 0 ; }
对应的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Ltmp0: .loc 1 30 22 prologue_end ## WeakDemo/main.m:30:22 callq _objc_autoreleasePoolPush Ltmp1: .loc 1 31 18 ## WeakDemo/main.m:31:18 movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rcx, %rdi movq %rax, -40(%rbp) ## 8-byte Spill callq *_objc_msgSend@GOTPCREL(%rip) leaq L__unnamed_cfstring_(%rip), %rcx .loc 1 31 12 is_stmt 0 ## WeakDemo/main.m:31:12 movq %rax, -24(%rbp) Ltmp2: .loc 1 33 43 is_stmt 1 ## WeakDemo/main.m:33:43 movq -24(%rbp), %rax .loc 1 33 36 is_stmt 0 ## WeakDemo/main.m:33:36 movq %rax, -32(%rbp) .loc 1 34 33 is_stmt 1 ## WeakDemo/main.m:34:33 movq -32(%rbp), %rsi .loc 1 34 13 is_stmt 0 ## WeakDemo/main.m:34:13 movq %rcx, %rdi movb $0, %al callq _NSLog xorl %edx, %edx movl %edx, %esi Ltmp3: .loc 1 36 5 is_stmt 1 ## WeakDemo/main.m:36:5 leaq -24(%rbp), %rdi callq _objc_storeStrong movq -40(%rbp), %rdi ## 8-byte Reload callq _objc_autoreleasePoolPop xorl %eax, %eax
编译器模拟代码:
1 2 3 4 id obj = objc_msgSend(NSObject , "new" ) id obj1 = obj NSLog(@"obj1:%@" , obj1) objc_storeStrong(&obj , nil )
可以看到仅仅是指针变量间的赋值,其他什么操作都没有。
示例 2:
1 2 3 4 5 6 7 8 9 int main(int argc, const char * argv[]) { @autoreleasepool { { id __unsafe_unretained obj1 = [NSObject new]; NSLog (@"obj1:%@" , obj1); } } return 0 ; }
编译器模拟代码:
1 2 3 4 id tmp = objc_msgSend(NSObject , "new" ) id obj1 = tmp objc_release(tmp ) NSLog(@"obj1:%@" , obj1)
这一次会发生崩溃,因为obj1已经变成了一个野指针。
__unsafe_unretained
的功能和 assign 是一样的,只不过assign 既可以修饰对象类型也可以修饰基础类型。注意:没有__assign
.
ps:个人感觉虽然 ARC 的优点是在合适的地方自动帮我们添加 retain 和 release,但作为开发者的我们如果没有查看汇编代码并不好确定它究竟会在哪些地方添加以及添加的啥函数调用,所以一些奇怪的现象可能会让你困惑。如果是 MRC 的话,我们就很清楚一个对象的释放与持有,弊端就是麻烦并且容易忘记 release导致内存泄漏。两种方案各有利弊,但总的来说还是 ARC 要更好一些。
疑问 Q0:ARC 适用范围?是否仅对 OC 文件起作用? 这个没查到资料,也不敢确定,但个人感觉 ARC 开启后,编译器应该只对 OC 源文件做处理。毕竟 C++也有它自己的内存管理方式。因此切换语言后还是要注意内存管理的不同。
Q1:把一个 weak 指针变量当做参数传给一个方法,当方法开始调用时,即使对象被其他线程释放了,此刻对象也不会被销毁,原因是什么?是形参这个指针变量又强引用了对象,导致对象不会被销毁,还是因为编译器插入的objc_loadWeakRetained函数?还是说两者都有? 两者都有,因为函数调用时会有一些压栈操作,如果没有objc_loadWeakRetained,那么在压栈操作期间,对象可能就销毁了。另外形参赋值的时候编译器也会插入objc_storeStrong这时形参指向的对象会被 retain ,当函数执行完成时,形参被废弃会调用release 方法。函数返回后继续执行objc_release(tmp);
,释放对象。
编译器对方法的入参对象进行retain和release:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 - (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event { [self testOjcParamCompilerInsertRetain]; } - (void )testOjcParamCompilerInsertRetain { XQModel *obj = [XQModel new]; [self hello:obj]; } - (void )hello:(XQModel *)model { NSLog (@"model:%@" , model); }
在 NSLog(@"model:%@", model);
处打好断点,如下图:
可以看到编译器会插入objc_storeStrong,从而对传入的 model 对象进行 retain,保证参数指向的对象在使用期间不被销毁。
可以po一下model的引用计数:已经变为2了。
1 2 (lldb) po NSLog (@"retain count = %ld\n" , CFGetRetainCount ((__bridge CFTypeRef )(model))); 2020 -09 -14 17 :18 :20.496168 +0800 iOSWeakDemo[53997 :4829011 ] retain count = 2
当方法执行完后,又会执行objc_storeStrong(&model, nil); 释放对象。
这个其实在MRC下会经常写这样的代码,对入参对象retain,使用完后release。只不过在ARC下编译器帮你自动完成了这些操作。
Q2:函数的调用过程究竟是怎样的? 参考:
C函数调用过程原理及函数栈帧分析
X86-64寄存器和栈帧
Q3:在 OC 中 [obj hello];
实际上是会变成objc_msgSend(obj, "hello")
的调用,-(void)hello;
方法其实是暗含了两个参数的一个是 id self
,一个是 sel _cmd
,问 self 是否会强引用对象? 从观察到的现象来看是没有的,举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 - (void )viewDidLoad { [super viewDidLoad]; } - (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event { [self testObjLife]; } #pragma mark- weak - (void )testObjLife { self .obj = [ARCPerson new]; NSLog (@"self.obj:%@" , _obj); dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ sleep(2 ); self .obj = nil ; NSLog (@"清除" ); }); [self .obj doTask]; } - (void )doTask { NSLog (@"开始计算" ); float f = 0.0 ; for (NSInteger i = 0 ; i < 60000000 ; i ++) { f = f + sin(sin(sin(time(NULL ) + i ))); } NSLog (@"计算完成" ); NSLog (@"model:%@ rs:%ld" , self , (long )f); }
当执行了self.obj = nil; //释放对象
后,对象就销毁了,然后崩溃在doTask里。说明 self 并没有强引用对象。
为啥使用 [self.obj doTask];
就不会崩溃呢?
在 [self.obj doTask];
处打好断点:
会发现 [self.obj doTask];
等价于:
1 2 3 4 id tmp = objc_msgSend(self , "obj" ); objc_retainAutoreleasedReturnValue(tmp); // 貌似只要前一句代码是非常规获得的对象都会有这个函数的出现。 objc_msgSend(tmp, "doTask" ); objc_release(tmp); // 这里是objc_release而不是objc_storeStrong是因为我们的代码不是"id obj = [self obj]; [obj doTask];"
单步执行,并进入到objc_retainAutoreleasedReturnValue函数里:
会发现objc_retainAutoreleasedReturnValue的参数就是tmp,继续跟踪的话会发现objc_retainAutoreleasedReturnValue内部调用了objc_retain,于是tmp的引用计数+1,所以使用 [self.obj doTask];
不会崩溃。
而直接使用示例变量 [_obj doTask];
则没有类似的objc_retainAutoreleasedReturnValue处理,因此2秒后当self.obj = nil;置为nil时,obj就会释放并销毁,doTask内部再访问self时会野指针崩溃。
打印日志:
1 2 3 4 5 6 2020 -09 -14 15 :45 :43 .518910 +0800 iOSWeakDemo[52902 :4783450 ] self.obj:<ARCPerson: 0 x6000039dc500>2020 -09 -14 15 :45 :43 .519025 +0800 iOSWeakDemo[52902 :4783450 ] 开始计算2020 -09 -14 15 :45 :45 .521771 +0800 iOSWeakDemo[52902 :4783607 ] ARCPerson dealloc2020 -09 -14 15 :45 :45 .521966 +0800 iOSWeakDemo[52902 :4783607 ] (null) 2 2020 -09 -14 15 :45 :45 .522120 +0800 iOSWeakDemo[52902 :4783607 ] 清除2020 -09 -14 15 :45 :59 .732099 +0800 iOSWeakDemo[52902 :4783450 ] 计算完成
可以看到还没计算完成,self指向的对象就已经销毁了。
Q4:MRC下dealloc重写时super调用的位置?[super dealloc]到底干了啥? 1 2 3 4 5 6 7 8 9 10 11 12 - (void )dealloc { NSLog (@"MRCPerson dealloc" ); NSLog (@"%@ 4" , self .cat); [super dealloc]; } - (void )dealloc { [super dealloc]; NSLog (@"MRCPerson dealloc" ); NSLog (@"%@ 4" , self .cat); }
dealloc方法就是释放对象自身的内存,以及析构它拥有的所有资源,比如实例变量对象。在MRC下,[super dealloc]必须放在最后。如果你放最开始的地方,那么执行完后对象就已经被销毁了,后面如果还有方法对象的地方将导致崩溃。
还是要看源码[super dealloc]。
参考:
Correct [super dealloc]
ARC下dealloc过程及.cxx_destruct的探究 4星
Q5:一个weak指针指向一个MRC对象,该MRC对象重写了dealloc但不调用[super dealloc],当该MRC对象销毁后,再打印该weak指针,会输出什么?nil还是对象地址? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - (void )viewDidLoad { [super viewDidLoad]; [self test_subclass_do_not_call_super_dealloc]; } - (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event { NSLog (@"weakObj:%@" , _wobj); } - (void )test_subclass_do_not_call_super_dealloc { MRCPerson *per = [[MRCPerson alloc] init]; _wobj = per; NSLog (@"_wobj:%@" , _wobj); } - (void )dealloc { NSLog (@"%@ dealloc" , self .class); }
答案:输出nil。
分析:对象销毁时如果不调用[super dealloc],理论上不会清除对象的weak表,自然weak指针也不会被置为nil,因此应该输出对象的地址,然而却输出了nil,这说明某个地方做了一些处理。从上面的分析我们已经知道使用weak指针时,编译器会在前面插入 objc_loadWeakRetained
,使用完后在后面插入 objc_release
。
因此 NSLog(@"weakObj:%@", _wobj);//断点打在这
等价于:
1 2 3 id tmp = objc_loadWeakRetained(&_wobj); NSLog(@"weakObj:%@" , tmp); objc_release(tmp);
而 objc_loadWeakRetained 源码里会rootTryRetain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 id objc_loadWeakRetained (id *location) { id obj; id result; Class cls; SideTable *table; retry: obj = *location; if (!obj) return nil; if (obj->isTaggedPointer()) return obj; table = &SideTables()[obj]; table->lock(); if (*location != obj) { table->unlock(); goto retry; } result = obj; cls = obj->ISA(); if (! cls->hasCustomRR()) { ASSERT(cls->isInitialized()); if (! obj->rootTryRetain()) { result = nil; } } else { if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) { BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL)) class_getMethodImplementation(cls, @selector(retainWeakReference)); if ((IMP)tryRetain == _objc_msgForward) { result = nil; } else if (! (*tryRetain)(obj, @selector(retainWeakReference))) { result = nil; } } else { table->unlock(); class_initialize(cls, obj); goto retry; } } table->unlock(); return result; }
rootTryRetain里会判断如果对象正在销毁,则返回nil,因此导致objc_loadWeakRetained也返回nil。于是tmp等于nil,自然打印的也是nil了。
我们可以重写retainWeakReference,返回yes,再运行一下,这时打印的就是对象的地址了。所以这里的nil并不是因为weak被清,weak指针被置为nil,而是因为objc_loadWeakRetained返回了nil。另外对于MRC来说重写dealloc方法一定要调用super的dealloc,否则销毁工作并没有完全结束,内存会有泄漏。
Q6:如何理解OC中内存管理相关的方法命名规则? 在OC中对于以alloc/init/new/copy/mutableCopy开头的方法创建返回的对象,是自己拥有的对象需要自己释放。其他开头的方法创建返回的对象,则不是自己拥有的对象不能自己释放,一般而言这样的对象是注册到自动释放池的对象,会在自动释放池销毁时释放,当然在一些特定情况下系统会优化性能避免注册到自动释放池,这一切对于开发者而言是无感知的,即使如此你依然没有拥有该对象不能自己释放。
比如myPerson方法:
1 2 3 4 + (instancetype )myPerson { ARCPerson *p = [[ARCPerson alloc] init]; return p; }
在 return p;
打好断点,如下图:
在返回时系统会将对象注册到自动释放池里。因此自己不拥有该对象,也就不能调用release方法。
而对于newPerson方法:
1 2 3 4 + (instancetype )newPerson { ARCPerson *p = [[ARCPerson alloc] init]; return p; }
如下图:
没有其他处理,返回的对象是自己拥有的,因此需要自己释放。
再比如getter方法:
1 2 3 4 5 @property (nonatomic , weak ) MRCPerson *wobj;- (MRCPerson *)wobj { return _wobj; }
对应的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 iOSWeakDemo`-[ViewController wobj]: 0x101e945e0 <+0 >: pushq %rbp 0x101e945e1 <+1 >: movq %rsp , %rbp 0x101e945e4 <+4 >: subq $0x10 , %rsp 0x101e945e8 <+8 >: movq %rdi , -0 x 8 (%rbp ) 0x101e945ec <+12 >: movq %rsi , -0 x 10 (%rbp ) 0x101e945f0 <+16 >: movq -0 x 8 (%rbp ), %rax -> 0x101e945f4 <+20 >: movq 0x968d (%rip ), %rcx 0x101e945fb <+27 >: addq %rcx , %rax 0x101e945fe <+30 >: movq %rax , %rdi 0x101e94601 <+33 >: callq 0x101e96024 0x101e94606 <+38 >: movq %rax , %rdi 0x101e94609 <+41 >: addq $0x10 , %rsp 0x101e9460d <+45 >: popq %rbp 0x101e9460e <+46 >: jmp 0x101e96006
因为这里是weak指针,所以这里调用的objc_loadWeakRetained函数。另外还会调用objc_autoreleaseReturnValue注册到自动释放池if needed。
如果是strong属性:
1 2 3 4 5 6 7 8 9 10 iOSWeakDemo`-[ViewController wobj]: 0x10529c5f0 <+0 >: pushq %rbp 0x10529c5f1 <+1 >: movq %rsp , %rbp 0x10529c5f4 <+4 >: movq %rdi , -0 x 8 (%rbp ) 0x10529c5f8 <+8 >: movq %rsi , -0 x 10 (%rbp ) 0x10529c5fc <+12 >: movq -0 x 8 (%rbp ), %rax -> 0x10529c600 <+16 >: movq 0x9689 (%rip ), %rcx 0x10529c607 <+23 >: movq (%rax , %rcx ), %rdi 0x10529c60b <+27 >: popq %rbp 0x10529c60c <+28 >: jmp 0x10529e02c
就只有objc_retainAutoreleaseReturnValue了。该函数的作用就是retain+autorelease。