探究ARC下dealloc实现

我是前言

目前正在看 oc 底层的东西,看了许多大牛的博客,发现有一些小问题:

  • runtime 的版本可能跟作者当时写的版本不一致
  • 许多方法一笔带过,因为基础知识的薄弱看不懂。。。
  • 没有标明苹果文档的出处

所以我打算解决上面的一些问题,然后重新发一版,当然大部分的内容还是原作者写的 。runtime 的源码为 objc4-646.tar.gz版本

进入正题

在 ARC 环境下,我们不需要主动的调用系统的析构函数 dealloc 就能够完成将对象以及父类的成员变量内存释放掉的操作:

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

问题来了:

  1. 这个对象成员变量(ivars)的释放操作去哪儿了?
  2. 没有主动调用 [super dealloc],那么是什么时候调用这个方法的?

ARC文档中对dealloc过程的解释

clang 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 方法在最后一次 release 后被调用,但此时实例变量(ivars)并未释放,父类的dealloc的方法将在子类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.

ARC下对象的实例变量在根类 [NSObject dealloc] 中释放(通常root class都是NSObject),变量释放顺序各种不确定(一个类内的不确定,子类和父类间也不确定,也就是说不用care释放顺序)


所以,我们不需要主动调用 [super dealloc] ,系统会自动调用,后面我们再讲这是怎么实现的。接下来我们来探究在根类 NSObject 析构时发生了什么

NSObject的析构过程

通过 runtime 源码,我们可以发现 NSObject 调用 dealloc 时会调用 _objc_rootDealloc(NSObject.mm 2071行) 继而调用object_dispose(objc-object.h 301行) 随后调用objc_destructInstance(objc-runtime-new.mm 6838行), 下面讲一下rootDealloc objc_destructInstance函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline void objc_object::rootDealloc()
{
assert(!UseGC);
if (isTaggedPointer()) return;

if (isa.indexed &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor)
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}

64位下,isa 指针的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
// ...
  • indexed(1 bit) 0 表示普通的 isa 指针,1 表示使用优化,即Tagged Pointer存储引用计数
  • has_assoc(1 bit) 表示该对象是否包含 associated object,如果没有,则析构(释放内存)时会更快
  • has_cxx_dtor(1 bit) 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构(释放内存)时更快
  • shiftcls(30 bits) 类的指针
  • magic(9 bits) 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
  • weakly_referenced(1 bit) 表示该对象是否有过 weak 对象,如果没有,则析构(释放内存)时更快
  • deallocating(1 bit) 表示该对象是否正在析构
  • has_sidetable_rc(1 bit) 表示该对象的引用计数值是否过大无法存储在 isa 指针
  • extra_jc(19 bits) 表示引用计数值减一后的结果。例如,如果对象引用计数为4,则extra_jc为3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}

return obj;
}

objc_destructInstance干了三件事情:

  1. 执行了一个 object_cxxDestruct 函数
  2. 执行_object_remove_assocations函数去除和这个对象 assocate 的对象(常用于类目中添加的属性 )
  3. 执行clearDeallocating, 清空引用计数并清除弱引用表,将所有使用__weak修饰的指向该对象的变量置为nil

所以,ARC 自动释放实例变量的地方就在 object_cxxDestruct 这个方法里面没跑了。

探究 object_cxxDestruct

上面找到的名为object_cxxDestruct的方法最终成为下面的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);

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

for ( ; cls != NULL; cls = _class_getSuperclass(cls)) {
if (!_class_hasCxxStructors(cls)) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_internal) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
_class_getName(cls));
}
(*dtor)(obj);
}
}
}

代码的大致意思是通过继承链(isa)向上递归调用 SEL_cxx_destruct这个函数的函数实现
这篇文章提到:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《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.

可以了解到cxx_destruct方法原本是为了 C++ 对象析构的,ARC 借用了这个方法插入代码实现了自动释放的工作。

通过实验找出 .cxx_destruct

1
2
3
4
5
6
7
8
@interface Father : NSObject
@property (nonatomic, copy) NSString *name;
@end

@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end

只有两个简单的属性,找个地方写简单的测试代码:

1
2
3
4
5
6
7
8
9
// start
{
// before new
Son *son = [Son new];
son.name = @"sark";
son.toys = @[@"sunny", @"xx"];
// after new
}
// gone

当过了大括号的作用域,son 对象就会被释放。所以在after new这行son对象初始化完成,在gone这行son对象被dealloc。

本次实验使用 NSObject+DLIntrospection 这个扩展来作用调试工具,通过它可以轻松打出一个类的方法,成员变量等。
将这个扩展引入工程,在 after new 处设置一个断点,在这里打印出 Son 类所有的方法名:

1
2
3
4
5
6
7
po [[Son class] instanceMethods]
<__NSArrayI 0x280982520>(
- (void)setToys:(id)arg0 ,
- (id)toys,
- (void).cxx_destruct
)

发现出现了.cxx_destruct这个方法,经过几次实验,发现:

  1. 只有在ARC下这个方法才会出现(试验代码的情况下)
  2. 只有当前类拥有实例变量时(不论是不是用property)这个方法才会出现,且父类的实例变量不会导致子类拥有这个方法
  3. 出现这个方法和变量是否被赋值,赋值成什么没有关系

使用 watchpoint 定位内存释放时刻

依然在 after new 断点处,输入 lldb 命令:

1
2
watchpoint set variable son->_name

name的变量加入watchpoint,当这个变量被修改时会触发trigger:
从中可以看出,在这个时刻,_name 从 0x0000000104ac5048 变成了0x0000000000000000,也就是nil,赶紧看下调用栈:
发现果然跟到了.cxx_destruct方法,而且是在objc_storeStrong方法中释放

刨根问底.cxx_destruct

知道了ARC环境下,对象实例变量的释放过程在 .cxx_destruct 内完成,但这个函数内部发生了什么,是如何调用 objc_storeStrong 释放变量的呢?
从上面的探究中知道,.cxx_destruct 是编译器生成的代码,那它很可能在clang前端编译时完成,这让我联想到clang的Code Generation,因为之前曾经使用clang -rewrite-objc xxx.m时查看过官方文档留下了些印象,于是google:
.cxx_destruct site:clang.llvm.org

结果发现clang的 doxygen 文档中 CodeGenModule 模块正是这部分的实现代码,cxx相关的代码生成部分源码在
http://clang.llvm.org/doxygen/CodeGenModule_8cpp-source.html
位于1827行,删减掉离题部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// EmitObjCIvarInitializations - Emit information for ivar initialization
/// for an implementation.
void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D)
{
DeclContext* DC = const_cast<DeclContext*>(dyn_cast<DeclContext>(D));
assert(DC && "EmitObjCIvarInitializations - null DeclContext");
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, 0,
DC, true, false, true,
ObjCMethodDecl::Required);
D->addInstanceMethod(DTORMethod);
CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
}

这个函数大概作用是:获取到 .cxx_destruct 的selector,创建 Method,然后加入到这个类的方法列表中,最后一行的调用才是真的创建这个方法的实现。这个方法位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 1354行,包含了构造和析构的 cxx 方法,继续跟随 .cxx_destruct,最终调用 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
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 = 0;

// 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),调用objc_storeStrong,从clang的ARC文档上可以找到 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];
}

在 .cxx_destruct 进行形如 objc_storeStrong(&ivar, null) 的调用后,这个实例变量就被release和设置成nil了

自动调用[super dealloc]的实现

按照上面的思路,自动调用 [super dealloc] 也一定是 CodeGen 的工作了,位于http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html 492行 StartObjCMethod 方法中:

1
2
if (ident->isStr("dealloc"))
EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());

上面代码可以得知在调用dealloc方法时被插入了代码,由FinishARCDealloc结构定义:

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
struct FinishARCDealloc : EHScopeStack::Cleanup {
void Emit(CodeGenFunction &CGF, Flags flags) override {
const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);

const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
const ObjCInterfaceDecl *iface = impl->getClassInterface();
if (!iface->getSuperClass()) return;

bool isCategory = isa<ObjCCategoryImplDecl>(impl);

// Call [super dealloc] if we have a superclass.
llvm::Value *self = CGF.LoadObjCSelf();

CallArgList args;
CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
CGF.getContext().VoidTy,
method->getSelector(),
iface,
isCategory,
self,
/*is class msg*/ false,
args,
method);
}
};

上面代码基本上就是向父类转发dealloc的调用,实现了自动调用[super dealloc]方法。

总结

  • ARC下对象的成员变量在编译器插入的.cxx_desctruct方法自动释放
  • ARC下[super dealloc]方法也由编译器自动插入
  • 所谓编译器插入代码过程需要进一步了解,还不清楚其运作方式

  • ARC环境,对象的实例变量将在根类 NSObject 的 dealloc 方法中释放内存
  • Father 的实例变量(如果有)将在它的 .cxx_desctruct方法中被释放,而 Son 的实例变量(如果有)将在它的 .cxx_desctruct方法中被释放
  • 子类在调用 dealloc 方法时会被插入代码,自动调用父类的 dealloc 方法

引用

ARC下dealloc过程及.cxx_destruct的探究
iOS内存管理之二

[self class]和[super class]

今天在学习runtime的时候,碰到一个有意思的题目,相信很多人都曾经看到过:

1
2
3
4
5
6
7
8
9
10
11
@implementation Son : Father

- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

输出为Son和Son,为什么呢?当我们使用 clang -rewrite-objc Son.m文件,可以看到

1
2
NSLog((NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_bf216e_mi_1, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_bf216e_mi_2, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"))));

这里需要明白以下几个概念:

方法中的隐藏参数 self

我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷的动态传入的。当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)
    之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。
1
2
static void _I_MyObject_test(MyObject * self, SEL _cmd) {
}

使用 clang 将代码转换成C++实现,我们可以看到方法的两个隐藏参数 self 和 _cmd

objc_msgSend 和 objc_msgSendSuper的定义

先给出 objc_msgSend 和 objc_msgSendSuper的定义:

1
2
3
id objc_msgSend(id self, SEL op, ...)
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

而 ‘objc_super’的定义如下:

1
2
3
4
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};

- (Class)class 方法的实现

1
2
3
- (Class)class {
return object_getClass(self);
}

现在再让我们看一下调用 [super class] 会发生什么

  • 当我们对 super 关键字发送消息时,编译器会创建一个 objc_super 的结构体,其中 receiver 仍旧为 self,而super_class为父类
  • 调用 objc_msgSendSuper()方法,会去 Father 的实例方法列表中寻找 class 这个方法,找不到,去 NSObject 中查找
  • 调用 NSObject 的 - (Class)class 方法,因为隐藏参数 self 为 son的实例,所以返回 Son

所以当我们调用 [self class] 和 [super class] 方法返回的都是 Son。

加入我们在Father中重载 -(Class)class 方法

1
2
3
- (Class)class {
return NSClassFromString(@"Father")
}

那么得到的输出将变成 Son 和 Father!