聊聊dealloc

前言


所有代码注释可在Objc-Runtime中查看

iOS开发中,我们经常会通过dealloc来判断对象实例是否被释放,依据是当对象实例的引用计数变为0时,运行时会调用对象实例的dealloc方法,我们可以利用该方法做一些扫尾的工作。

dealloc调用时机


Objective-C的引用计数管理使用两种方式相结合,sidetableisa指针(指针并不是对象的真正内存地址,而是某些位用来进行了一些标志位的存放);接下来,我将以sidetable进行release来讨论dealloc的调用,直接上代码,如下sidetable_release(下文所有都会用sidetable_release来讨论)函数会在给对象发送release消息的时候调用,sidetable_release方法首先获取对象的引用计数,对引用计数相关标志位做操作,若对象实例可以被释放,将通过objc_msgSend发送SEL_dealloc消息,既调用对象的dealloc方法。

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 uintptr_t objc_object::sidetable_release(bool performDealloc) { #if SUPPORT_NONPOINTER_ISA assert(!isa.nonpointer); #endif SideTable& table = SideTables()[this];   bool do_dealloc = false;   table.lock(); RefcountMap::iterator it = table.refcnts.find(this); if (it == table.refcnts.end()) { do_dealloc = true; table.refcnts[this] = SIDE_TABLE_DEALLOCATING; } else if (it->second < SIDE_TABLE_DEALLOCATING) { // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it. do_dealloc = true; it->second |= SIDE_TABLE_DEALLOCATING; } else if (! (it->second & SIDE_TABLE_RC_PINNED)) { it->second -= SIDE_TABLE_RC_ONE; } table.unlock(); // 进行释放操作,调用dealloc if (do_dealloc && performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return do_dealloc; }

dealloc方法的实现如下:

1 2 3 - (void)dealloc { _objc_rootDealloc(self); }

直接调用_objc_rootDealloc方法来做处理,我们省略一些细节处理,通常情况下,dealloc方法最终会调用objc_dispose方法,内部又调用objc_destructInstance方法来进行析构操作,析构完成后将内存释放掉。

1 2 3 4 5 6 7 8 9 10 11 id object_dispose(id obj) { if (!obj) return nil;   objc_destructInstance(obj); // 做完各种析构操作后释放obj的内存 free(obj);   return nil; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void *objc_destructInstance(id obj) { if (obj) { // Read all of the flags at once for performance. bool cxx = obj->hasCxxDtor(); bool assoc = obj->hasAssociatedObjects();   // This order is important. if (cxx) object_cxxDestruct(obj); // 调用C++析构器 if (assoc) _object_remove_assocations(obj); // 移除对象相关的关联引用 obj->clearDeallocating(); // 进行ARC相关操作,如weak置nil,清理计数位 }   return obj; }

并发赋值


考虑如下代码,我们来模拟并发的对变量obj进行赋值。

1 2 3 4 5 6 7 8 9 10 __block NSObject *obj = [NSObject new]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ while (YES) { obj = [NSObject new]; } });   while (YES) { obj = [NSObject new]; }

 

执行如上代码,我们发现,很快程序就会崩溃,异常为EXC_BAD_ACCESS,既访问已释放的内存地址,异常栈如下,在调用objc_msgSend发送SEL_dealloc方法时异常,而该方法正是在如上的objc_object::sidetable_release中被调用的,也就是release方法调用过程中。最终的原因就是对已释放的对象实例再次进行release操作。

1 2 3 4 5 6 7 8 9 10 11 12 13 0x106463a00 <+156>: callq 0x1064653e8 ; objc::DenseMapBase<objc::DenseMap<DisguisedPtr<objc_object>, unsigned long, true, objc::DenseMapInfo<DisguisedPtr<objc_object> > >, DisguisedPtr<objc_object>, unsigned long, objc::DenseMapInfo<DisguisedPtr<objc_object> >, true>::FindAndConstruct(DisguisedPtr<objc_object> const&) 0x106463a05 <+161>: movq $0x2, 0x8(%rax) 0x106463a0d <+169>: movl -0x2c(%rbp), %ebx 0x106463a10 <+172>: movq %r15, %rdi 0x106463a13 <+175>: callq 0x1064669fa ; symbol stub for: os_unfair_lock_unlock 0x106463a18 <+180>: testb %bl, %bl 0x106463a1a <+182>: je 0x106463a2e ; <+202> 0x106463a1c <+184>: leaq 0x55a8ad(%rip), %rax ; SEL_dealloc 0x106463a23 <+191>: movq (%rax), %rsi // 在这访问了已释放的内存地址 0x106463a26 <+194>: movq %r14, %rdi 0x106463a29 <+197>: callq 0x106465940 ; objc_msgSend 0x106463a2e <+202>: movl $0x1, %eax 0x106463a33 <+207>: jmp 0x106463a4c ; <+232>

为什么会导致这样的结果呢?原因其实是,对属性的赋值操作并不是原子操作,对属性的赋值其实是调用属性的setter方法,默认setter代码实现如下:

1 2 3 4 5 6 7 - (void)setObj:(NSObject *)obj { if (obj != _obj) { // 1 id oldValue = _obj; // 2 _obj = [obj retain]; // 3 [oldValue release]; // 4 } }

我们考虑两个线程同时进行setObj:赋值操作,当走到第4步时,两个线程同时尝试进行release操作,结果是一个线程成功的释放对象,而另一个线程会在release函数调用过程中访问已经释放的内存区域,这就导致了崩溃。

dealloc在哪个线程被调用


dealloc并不总是在主线程中被调用,从如上sidetable_release方法,我们可得知,其调用线程为最后一个调用release方法的线程,当需要释放对象时,向对象实例发送SEL_dealloc(即dealloc)消息。

也就是说,dealloc方法有可能在任何线程被调用,这就需要注意一点,就是在dealloc中进行UIKit相关API的操作(UIKit相关API只能在主线程操作)。

参考


  1. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmPractical.html#//apple_ref/doc/uid/TP40004447-SW13
上一篇:《Soft-NMS – Improving Object Detection With One Line of Code》论文翻译


下一篇:信息收集及kali安装遇到的问题