前言


所有代码注释可在​​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​​只能在主线程操作)。

参考


  1. ​https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmPractical.html#//apple_ref/doc/uid/TP40004447-SW13​



  • 本文作者: 钟武


------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。