聊聊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
14
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