前言
所有代码注释可在Objc-Runtime中查看
在iOS
开发中,我们经常会通过dealloc
来判断对象实例是否被释放,依据是当对象实例的引用计数变为0时,运行时会调用对象实例的dealloc
方法,我们可以利用该方法做一些扫尾的工作。
dealloc调用时机
Objective-C
的引用计数管理使用两种方式相结合,sidetable
和isa
指针(指针并不是对象的真正内存地址,而是某些位用来进行了一些标志位的存放);接下来,我将以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
只能在主线程操作)。
参考
- 本文链接: https://zhongwuzw.github.io/2017/09/21/聊聊dealloc/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!