Save&Load

Save The World, Load The Game

0%

iOS底层探索 - ARC下的dealloc

前言

解决的crash越多,越觉得了解对象的完整生命周期是一件很有必要的事情。所以有了这个系列,至于为什么先说dealloc,那是因为对象在创建的时候一般不会有问题,但是对象在释放的时候往往会有很多问题。

dealloc历史演进

MRC时代的dealloc

在MRC时代,我们需要手动管理对象的释放。例如:

1
2
3
4
5
6
7
8
- (void)dealloc {
self.array = nil;
self.dictionary = nil;
// ... //
// 非Objc对象内存的释放,如CFRelease(...)
// ... //
[super dealloc];
}

简单来说就是先释放自己内部的成员变量和非Objc对象,然后再调用[super dealloc],继续父类的析构。

ARC时代的dealloc

但是到了ARC时代,代码就变成了:

1
2
3
4
5
- (void)dealloc {
// ... //
// 非Objc对象内存的释放,如CFRelease(...)
// ... //
}

可以看到相较于MRC,少了两部分:1. 释放内部成员变量(ivars) 2. 调用[super dealloc]

这是为什么呢,我们看一下ARC官网文档中的描述:

A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.

大意是:一个类中可能会有一个默认的dealloc实例方法,这个方法在实例和成员变量(ivars)释放之前被调用。当这个方法返回时会自动调用父类的dealloc。这里我们就知道了[super dealloc]是由系统自动调用的。

The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.

大意是:成员变量(ivars)会在root class(一般为NSObject)的dealloc方法中销毁。成员变量的释放顺序是不确定的。这回我们也知道了成员变量是在root class的dealloc方法中销毁的,不需要我们手动去销毁。

dealloc实现

dealloc调用栈

既然dealloc的时候系统帮我们做了这么多事情,那具体是怎么实现的呢,就要看一下runtime的源码了。从网上找到runtime 723版本的源码,我们在NSObject.mm中找到了dealloc方法。

1
2
3
4
5
6
7
8
9
10
11
- (void)dealloc {
_objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
assert(obj);

obj->rootDealloc();
}

我们可以看到dealloc只是简单的调用了_objc_rootDealloc_objc_rootDealloc又调用了rootDealloc。一路跟踪下去,rootDealloc调用object_dispose,最后调用objc_destructInstance。详细来看objc_destructInstance:

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);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}

return obj;
}

可以看到代码按照顺序执行了三步操作:

  1. 如果hasCxxDtor,则执行object_cxxDestruct,调用C++的析构函数,清理ARC成员变量。
  2. 如果hasAssociatedObjects,则执行_object_remove_assocations,字面意思移除assocate的对象。
  3. 最后执行clearDeallocating,清空引用计数表并清除弱引用表,将所有weak引用指nil。这回我们知道weak引用是什么时候被置为nil了。

object_cxxDestruct

一个一个来,先来看object_cxxDestruct

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
void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}

static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);

// Call cls's dtor first, then superclasses's dtors.

for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}

object_cxxDestruct又调用了object_cxxDestructFromClass,这里面主要做了一件事,那就是递归查找父类中名为SEL_cxx_destruct的方法并调用。那么SEL_cxx_destruct是啥呢,可惜的是在runtime源码中没有找到具体的定义,只知道这是一个全局的SEL变量。

_object_remove_assocations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}

做的事情大致上就是找到对象的所有关联对象,然后放到一个数组中,接着遍历整个数组释放每个对象。

clearDeallocating

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline void 
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}

assert(!sidetable_present());
}

这里主要是判断一下isa.nonpointernonpointer这个属性主要是arm64架构引入的一个概念,对我们理解流程没有大的影响,两个分支最后都会调用到weak_clear_no_lock方法里面,这个代码比较长就不贴了,主要做的就是从weaktable中找到对应的引用,将引用置为nil,再从weaktable中移除。感兴趣的可以自己看下源码。

隐藏的.cxx_destruct

现在就只剩下SEL_cxx_destruct到底做了什么不清楚了,经过搜索在runtime源码的注释中找到一些线索:

1
2
// object may have -.cxx_destruct implementation?
bool hasCxxDtor();

hasCxxDtor方法的注释中,我们可以看到.cxx_destruct这样一个方法。然后上网搜了一些应该是这个方法没错了。

《Effective Objective-C 2.0》中提到:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

也就是说编译器在看到一个对象中有C++对象时,就会生成一个.cxx_destruct的方法用于析构,ARC借用这个方法插入了清理成员变量的操作。

为了进一步的验证,我们用Xcode断点来看一下实际情况,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@interface Fruit : NSObject

@property (nonatomic, strong) NSString *name;

@end

{

Fruit *fruit = [[Fruit alloc] init];
fruit.name = @"apple";
//end
}

end出设置断点,观察name成员变量:watchpoint set variable fruit->_name
-w808
-w843

然后继续断点,会发现name0x0000000105524088被修改为0x0000000000000000,即nil。此时观察调用栈:
-w396

可以看到[Fruit .cxx_destruct]的调用,说明object_cxxDestructFromClass之后确实是调用了.cxx_destruct,并且是在.cxx_destruct中释放的成员变量。

深入clang

可惜的是.cxx_destruct的具体实现并不在runtime的源码中,不过通过查找相关资料,发现.cxx_destruct的实现是在clang前端编译时完成的,好在clang也是开源的,那么就好说了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D) {
// We might need a .cxx_destruct even if we don't have any ivar initializers.
if (needsDestructMethod(D)) {
IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
ObjCMethodDecl *DTORMethod =
ObjCMethodDecl::Create(getContext(), D->getLocation(), D->getLocation(),
cxxSelector, getContext().VoidTy, nullptr, D,
/*isInstance=*/true, /*isVariadic=*/false,
/*isPropertyAccessor=*/true, /*isImplicitlyDeclared=*/true,
/*isDefined=*/false, ObjCMethodDecl::Required);
D->addInstanceMethod(DTORMethod);
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
D->setHasDestructors(true);
}

//此处省略创建构造函数的代码
}

首先判断needsDestructMethod,这个方法就是遍历类的所有成员变量(ivars),如果变量是需要析构的,那么就会返回true。接下来就是创建了一个名为.cxx_destruct的方法,把这个实例方法加入到这个类中,并将HasDestructors设置为true。而.cxx_destruct的具体实现就是由GenerateObjCCtorDtorMethod这个方法生成的。继续看GenerateObjCCtorDtorMethod发现只是简单的判断了一下,然后就调用到了emitCXXDestructMethod

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
static void emitCXXDestructMethod(CodeGenFunction &CGF,
ObjCImplementationDecl *impl) {
CodeGenFunction::RunCleanupsScope scope(CGF);

llvm::Value *self = CGF.LoadObjCSelf();

const ObjCInterfaceDecl *iface = impl->getClassInterface();
for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin();
ivar; ivar = ivar->getNextIvar()) {
QualType type = ivar->getType();

// Check whether the ivar is a destructible type.
QualType::DestructionKind dtorKind = type.isDestructedType();
if (!dtorKind) continue;

CodeGenFunction::Destroyer *destroyer = nullptr;

// Use a call to objc_storeStrong to destroy strong ivars, for the
// general benefit of the tools.
if (dtorKind == QualType::DK_objc_strong_lifetime) {
destroyer = destroyARCStrongWithStore;

// Otherwise use the default for the destruction kind.
} else {
destroyer = CGF.getDestroyer(dtorKind);
}

CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);

CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
cleanupKind & EHCleanup);
}

assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
}

这里主要做的就是遍历当前对象中的所有成员变量(ivars),如果是需要析构的且是__strong类型的,则调用objc_storeStrong去释放这个变量。其他的则调用默认的析构函数去释放。可惜的是没有找到objc_storeStrong的源代码,只是找到了官方定义的伪代码:

1
2
3
4
5
6
void objc_storeStrong(id *object, id value) {
id oldValue = *object;
value = [value retain];
*object = value;
[oldValue release];
}

基本就是先赋值再释放的过程,这里我们传入的参数object就是要释放的对象,value就是nil。也就是说对象先被置为nil,再释放了原有的值。

其实objc_storeStrong这个方法是对象赋值时用到的操作,这里用来释放对象是一个比较trick的操作。不过我们从这里就可以知道多线程操作读写同一个对象为什么是不安全的,因为当线程A在赋值的时候,线程B可能正好拿到了对象的oldValue,因此线程B后面在操作oldValue的时候其实已经被释放了,导致crash。

总结

  • 对象销毁的时候内部成员变量(ivars)的释放和[super dealloc]的调用都是由系统完成的
  • 内部成员变量(ivars)的释放是通过在编译阶段插入的.cxx_destruct方法来完成的
  • 关联对象清除和weak引用置为nil也是在dealloc的时候做的
  • 三个析构方法的执行顺序为什么是这样的?后面可以再深入想想

参考