内存是程序运行的原材料,使用不同的方法将内存加工成各式各样的产品。原材料是产品质量的基础。所以对于内存的使用非常重要。
OC在iOS系统中内存管理方式是引用计数,区别于java的垃圾回收和C++和C的开发人员管理内存释放。
在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。
引用计数有三个要点:
- 引用计数需要** 一个 “计数的值”**
- 引用计数需要** 增加引用计数的方法**
- 引用计数需要** 减少引用计数的方法**
1.runtime内存管理数据结构
引用计数的三点要求对于OC而言依然是不可或缺的,OC肯定会有相应的结构体和方法去满足上面的这些要求。从现在开始我们切换到ARC,来讨论OC的内存管理原理
OC中的大多数对象都继承NSObject,我们来看一段代码,这段代码中OC如何做到对a所指向的对象进行引用计数管理
{
id a = [[NSObject alloc] init];
id b = a;
}
我们将上面代码编译后看,编译器做了什么事情,下面是简化代码
//1.alloc
movq 0x3211(%rip), %rsi; (void *)0x000000010d142e58: NSObject
movq 0x31c2(%rip), %rdi ; "alloc"
callq 0x10c7a2952 ; symbol stub for: objc_msgSend
//2.init
movq 0x31b3(%rip), %rsi ; "init"
callq 0x10c7a2952 ; symbol stub for: objc_msgSend
//3.b = a
callq 0x10c7a2964 ; symbol stub for: objc_retain
//4.出了作用域,销毁b,
callq 0x10c7a2976 ; symbol stub for: objc_storeStrong
//5.出了作用域,销毁a,
callq 0x10c7a2976 ; symbol stub for: objc_storeStrong
上面代码的意思是生成一个NSObject对象,用指针a,b进行持有。而在编译后的代码中我们猜测:
- objc_retain 是OC引用计数中三要素之一的引用计数增加。
- objc_retain objc_storeStrong两者内部肯定有对 引用计数“计数值”的减少操作。
1.1 OC引用计数数据结构和方法
通过上面的讲述,我们得到关键点objc_retain和objc_storeStrong,那么我们从这二者入手,首先是两者的源代码
//objc_retain 持有
id objc_retain(id obj)
{
if (!obj) return obj; //nil
if (obj->isTaggedPointer()) return obj; //tagged是内存优化
return obj->retain();
}
//objc_storeStrong
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
//先持有新值
objc_retain(obj);
*location = obj;
//释放老值
objc_release(prev);
}
//objc_release,释放
void objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return; //内存优化
return obj->release();
}
1.isTaggedPointer是系统的内存优化,对于NSNumber,指针指向的不是对象的地址,指针本身的地址就是值。
objc_retain 是引用计数+1操作,而objc_storeStrong中调用的objc_release是-1操作,那么objc_retain和objc_release肯定有对引用计数值的操作,我们拿出objc_retain的详细调用栈看看
inline id objc_object::retain()
{
// UseGC is allowed here, but requires hasCustomRR.
assert(!UseGC || ISA()->hasCustomRR());
assert(!isTaggedPointer());
//如果没有实现retain/release,就会调用sidetable_retain()
if (! ISA()->hasCustomRR()) {
return sidetable_retain();
}
//上面的都不满足,直接调用该类的retain,和release的方法
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
//NSobjct协议
@protocol NSObject
- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE
ISA()->hasCustomRR()表明该类是否自定义了release和retain方法,在ARC下是不允许使用release和retain的更何况自定义。
我们发现,objc_object::retain()先断言处理掉两种异常情况:1.对象使用了GC(垃圾回收)但并没有自定义release和retain等方法;2. TaggedPointer;因为TaggedPointer是不会调用retain方法的。其次,当没有自定义release和retain 等方法的时,就使用sidetable_retain()。最后,对于自定义了release和retain 等方法的类,就直接调用他们的retain方法。
但还是没有看到引用计数真正+1的地方,但是ARC下上面的代码会进入sidetable_retain(), sidetable_retain中会有什么样的情况发生?先看代码
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
//1.根据this(C++this,类似于OC的self) SideTables中取出一个SideTable容器
SideTable& table = SideTables()[this];
//2.给SideTable试着加锁
if (table.trylock()) {
//3.根据this从table的refcnts中取出引用计数“计数值”
size_t& refcntStorage = table.refcnts[this];
//4.值合法的情况下,增加SIDE_TABLE_RC_ONE
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
return sidetable_retain_slow(table);
}
千呼万唤始出来,犹抱琵琶半遮面。上面的代码终于揭露出引用计数“计数值”面目。上面代码分析如下:
- 第一步,先从 全局SideTables Map中以对象自己为key,取出一个SideTable。那说明程序运行期间每一个对象都会有一个一一对应的SideTable存储在SideTables()这个map中。
- 第二步,拿到对象对应的table,先尝试对table加锁。那么我们可以知道SideTable中肯定有一个锁的机制。
- 第三步,从table的refcnts Map中以对象的指针地址为key,取出类型为size_t 的引用计数“计数值”。 那么可以知道SideTable中存储了一个refcnts Map,用来存储真正的引用计数值
- 第四步,检验计数值是否合法,如果合法,对计数值 增加一个SIDE_TABLE_RC_ONE,这个SIDE_TABLE_RC_ONE就是引用计数的1。既然引用计数值增加1用的是SIDE_TABLE_RC_ONE,那么说明引用计数值除了携带计数值意外,还携带其他的信息
上面四步中加粗部分使我们对 对象的内存管理数据结构的部分功能总结。下面我们来看一下和对象一一对应的SideTable具体定义:
//简化后的代码。
struct SideTable {
spinlock_t slock; //1.锁
RefcountMap refcnts; //2.引用计数值字典
weak_table_t weak_table; //3.存储所有指向对象的weak指针
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
};
SideTable作为和对象一一对应的内存管理辅助数据结构,其有三个重要成员
- 1.slock,在多线程编程中对SideTable操作时,用来加锁
- 2.RefcountMap 一个Map,以对象指针为key,存储了size_t的引用计数计数值
- 3.weak_table 用来存储所有指向对象的weak指针。
这块我们重点讲解RefcountMap 中引用计数数值代表的意义,weak_table到__weak处讲解。
RefcountMap
RefcountMap 根据对象的地址,存储了size_t类型计数值
size_t在32位系统上定义为 unsigned int;在64位系统上定义为 unsigned long,也就是说 size_t 在32位系统上是32位,在64位上是64位,我们以32位为例,将引用计数化为32位如下图
我们在id objc_object::sidetable_retain()中提到了引用计数+1 用SIDE_TABLE_RC_ONE表示,其实还有其他的,我们都列出来
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))
#define WORD_BITS 32
根据图和代码
- 计数值的 第0位表示 是否有weak指针指向该对象。该位为1表示有weak指针指向,为0表示没有。这个标记的具体用途后面会讲。
- 计数值的 第1位表示 该对象是否正在执行dealloc。该位为1表示正在执行,为0表示没有。这个标记位为了安全,防止dealloc执行时进行其他持有操作。
- 计数值的 第2位到31位 表示引用计数的个数。 引用计数+1 实际执行的是 SIDE_TABLE_RC_ONE (1UL<<2),表明引用计数单位一(值为4),那么第0位和第1位的值不会对计数有影响。
之所以这么做是为了节省内存,用一个变量代替三个变量使用。
现在我们知道了OC为了辅助引用计数方式的内存管理,给每一个对象生成了一个sideTable,里面的RefcountMap保存了对象的引用计数。还包括了其他的信息。
2.__weak
__weak是为了防止循环引用并且对象消失后,再次调用对象的方法或者属性不会产生crash。接下来我们讲解weak的实现原理。上代码
{
id __weak weakObj = [[NSObject alloc] init];
}
编译后代码如下
//1.alloc
movq 0x2959(%rip), %rsi ; (void *)0x000000010b125e58: NSObject
movq 0x2922(%rip), %rdi ; "alloc"
callq 0x10a785a56 ; symbol stub for: objc_msgSend
//2.init
movq 0x2913(%rip), %rsi ; "init"
callq 0x10a785a56 ; symbol stub for: objc_msgSend
//3.initWeak
callq 0x10a785a4a ; symbol stub for: objc_initWeak
/4.release
callq 0x10a785a62 ; symbol stub for: objc_release
//5.destroyWeak
callq 0x10a785a3e ; symbol stub for: objc_destroyWeak
编译后的代码分为五部分:1.alloc。2.init。3不知道(objc_initWeak)。4.objc_release。5.不知道(objc_destroyWeak)。
对于weak,上面的编译代码中有两个特殊的点1.objc_initWeak,2. objc_destroyWeak。那么两个函数中肯定有weak的实现操作。
2.1 weak实现原理
下面是objc_destroyWeak,objc_initWeak。部分简化后的代码
//1.初始化weak
id objc_initWeak(id *object, id value) {
*object = nil;
return objc_storeWeak(object, value);
}
//2.weak的内存管理函数
id objc_storeWeak(id *location, id newObj)
{
return storeWeak
(location, (objc_object *)newObj);
}
//3.weak实际内存管理代码,简化后的代码
id storeWeak(id *location, objc_object *newObj)
{
assert(HaveOld || HaveNew);
if (!HaveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
//第一步
retry:
if (HaveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (HaveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 第二步
if (HaveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
//第三步
if (HaveNew) {
newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
(id)newObj, location,
CrashIfDeallocating);
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;
}
SideTable::unlockTwo(oldTable, newTable);
return (id)newObj;
}
一连串的函数调用,其中objc_initWeak的调用如下
objc_initWeak (将object先值为空)
|objc_storeWeak(传入一些其他业务参数)
| storeWeak(真正开始操作)
而 重点在storeWeak,其传入weak指针location,和要指向的对象newObj。然后做三步操作
- 第一步,先取出location指向的旧对象oldObj和要指向的对象newObj它们的内存辅助管理对象SideTable(第一节我们讲过了)。
- 第二步,如果旧对象oldObj的SideTable存在,就执行weak_unregister_no_lock函数,从字面上理解,这个函数应该是将location从旧对象的SideTable的weak_table(存放weak指针的表,weak_table一会讲解,)中撤出。
- 第三步,如果新对象newObj的SideTable存在,就将location注册到新对象newObj的SideTable的weak_table中,然后给newObj的SideTable的内存引用计数变量打上被weak指针了的标记。最后更改location的指向值。
上面三步的重点都指向SideTable的weak_table,并且weak_table应该可以存储很多weak指针,下面我们看一下weak_table的结构
//简化后的代码
struct weak_table_t {
weak_entry_t *weak_entries;//weak_entry_t的一维数组
size_t num_entries;//数组的个数
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct weak_entry_t {
DisguisedPtr referent; //可以包裹objc_object类型指针的一个struct
union {
struct {
weak_referrer_t *referrers;
};
};
};
weak_table_t
- weak_entries 指向weak_entry_t的指针,系统用其指向一个weak_entry_t的数组。
- num_entries表明了这个数组数组中有多少个元素。
weak_entry_t
- referent 用来包裹objc_object类型指针的结构体。
从两个数据结构和使用来看,1.指向对象的weak指针会被包裹成 weak_entry_t,可以认为weak_entry_t代表weak指针,2.weak_entry_t又会被存储在weak_table_t的weak_entries指向的数组中。
weak的实现原理靠对这个表进行操作来完成。源码中提供了对weak_table_t的各种各样的操作函数,具体的内容就不再讲述了,因为都是对weak_table_t中weak_entries的操作,对我们了解weak原理没有影响,这里就只列举一下。
//将weak指针注册到对象的sideTable的weak_table的weak指针数组中
id weak_register_no_lock(weak_table_t *weak_table, id referent,
id *referrer, bool crashIfDeallocating);
//和weak_register_no_lock相反,将weak指针移除
void weak_unregister_no_lock(weak_table_t *weak_table, id referent, id *referrer);
//weak_read_no_lock是在weak指针使用的时候,需要调用,下面我们讲解weak指针的使用时候会说道。
id weak_read_no_lock(weak_table_t *weak_table, id *referrer);
//清楚对象的weak_table_t中所有的weak指针
void weak_clear_no_lock(weak_table_t *weak_table, id referent);
2.2weak指针的使用
上面我们介绍了weak指针的存储结构,我们结合weak指针的使用,来看一下weak指针的原理。老习惯,上代码
{
id __weak weakObj = [[NSObject alloc] init];
[weakObj class];
}
编译后的结果
//1.alloc, init
movq 0x33f1(%rip), %rdi ; (void *)0x0000000104e34e58: NSObject
movq 0x33a2(%rip), %rsi ; "alloc"
movq 0x225b(%rip), %rax ; (void *)0x0000000104a7fac0: objc_msgSend
movq 0x3396(%rip), %rsi ; "init"
//2.initWeak
callq 0x1044927b6 ; symbol stub for: objc_initWeak
//3.NSObject 没有人持有,编译器增加release
movq 0x222f(%rip), %rsi ; (void *)0x0000000104a7cd20: objc_release
//4.使用weak指针前,先retain
callq 0x1044927bc ; symbol stub for: objc_loadWeakRetained
//5.调用
movq 0x335a(%rip), %rdi ; "class"
//6.第四步retain了一次,所以要释放一次
callq 0x1044927ce ; symbol stub for: objc_release
//7.销毁weak指针
callq 0x1044927aa ; symbol stub for: objc_destroyWeak
上面代码总共7步,其中最为特别的是第4-6步,系统在使用weak指针指向的对象时,先调用了objc_loadWeakRetained, 然后在第6步 release。那说明使用weak先得强持有一下,然后才能使用。为甚系统要怎么做,不麻烦吗? 看完objc_loadWeakRetained之后就知道为什么了。
id objc_loadWeakRetained(id *location)
{
id result;
SideTable *table;
retry:
//1.先取出weak指针指向的对象
result = *location;
//2.对象为nil,表明weak指向nil了。
if (!result) return nil;
//3.如果指向对象不为空,取出容器
table = &SideTables()[result];
//4.加锁,很关键,因为不同现成的操作会想想取值,甚至crash
table->lock();
if (*location != result) {
table->unlock();
goto retry;
}
//5.读取值
result = weak_read_no_lock(&table->weak_table, location);
table->unlock();
return result;
}
objc_loadWeakRetained分了五部分
- 1.先取出weak指针指向的对象。
- 2.空判断
- 3.根据对象取出对象的SideTable
- 4.SideTable加锁
- 5.调用weak_read_no_lock,传入对象的SideTable的weak_table,和weak指针。读取location指向的对象
五步中重要的最后一步,调用weak_read_no_lock读取值,上面第一步已经得到了指向的对象,为什么还要查找weak_table?先留个疑问,我们看一下weak_read_no_lock干了什么
id weak_read_no_lock(weak_table_t *weak_table, id *referrer_id)
{
objc_object **referrer = (objc_object **)referrer_id;
objc_object *referent = *referrer;
if (referent->isTaggedPointer()) return (id)referent;
//1.查看referrer_id指针是否在weak_table中
weak_entry_t *entry;
if (referent == nil ||
!(entry = weak_entry_for_referent(weak_table, referent)))
{
return nil;
}
//2.判断对象是否有自定义的release/retain方法,hasCustomRR(第一节备注过)
if (! referent->ISA()->hasCustomRR()) {
//3.ARC下是没有自定义的,所以调用rootTryRetain方法
if (! referent->rootTryRetain()) {
return nil;
}
}
else {
//ARC下不会调用这块代码,影响篇幅,所以删除
}
return (id)referent;
}
//tryRetain
inline bool objc_object::rootTryRetain()
{
assert(!UseGC);
if (isTaggedPointer()) return true;
return sidetable_tryRetain();
}
//sidetable_tryRetain
bool objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
//1.取出SideTable
SideTable& table = SideTables()[this];
bool result = true;
//2.查找引用计数 “计数值”
RefcountMap::iterator it = table.refcnts.find(this);
//3.如果根本没有存,表明是alloc,init第一次,retain成功
if (it == table.refcnts.end()) {
table.refcnts[this] = SIDE_TABLE_RC_ONE;
}
//4.计数值和SIDE_TABLE_DEALLOCATING相等,表明正在执行dealloc方法。retain失败
else if (it->second & SIDE_TABLE_DEALLOCATING) {
result = false;
}
//5.计数值没有超过一定范围,那么计数值增加SIDE_TABLE_RC_ONE,retain成功
else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second += SIDE_TABLE_RC_ONE;
}
return result;
}
为了不影响篇幅我们删除了一些无用的代码。并且一下子给出了该过程的全部代码。
weak_read_no_lock三步
- 1.先判定weak指针是否存在在weak指针执行的对象的weak_table中,如果没有,返回nil
- 2.判断对象是否自定义过 retain/release方法,如果没有执行if中的语句
- 3.ARC下对象时不能自定义releas和retain方法该方法的,所以调用对象的rootTryRetain(),该方法返回true,表明retain成功,然后就能在weak_read_no_lock返回weak指针指向的对象。
weak_read_no_lock最终又调用了tryretain,而tryretain又有5步调用
- 1.取出对象的SideTable
- 2.从SideTable根据对象取出引用计数计数值
- 3.计数值为空,表明是第一次引用,肯定是alloc,init,所以引用计数数值为SIDE_TABLE_RC_ONE(1),retain成功
- 4.计数值的值为SIDE_TABLE_DEALLOCATING,表明对象在执行dealloc,这个时候是不能引用的,,retain失败
- 5.计数值没有超过范围,直接计数值增加SIDE_TABLE_RC_ONE(1),引用成功。
通过上面的过程,weak指针的使用过程OC饶了几个圈。为什么要这么做?我们考虑下下面的场景。
线程A 正在使用 weak指针指向的对象,正在调用方法,线程B突然释放了weak指针指向的对象,那么线程A可能用着用着就crash了。
为了防止这种情况出现,weak指针的使用一定要先拿到对象,并持有住,然后再使用。其次,系统还在持有weak指针指向的对象的时候,进行了合法性检验,判断了weak指针是否在表中和对象是否正在执行dealloc。
weak指针的每一句使用代码都会执行上面的全过程,也即是说objc_loadWeakRetained会多次调用,所以尽量避免使用weak,除非真正需要。
知道了weak指针存储和weak指针的使用,weak指针还差最后一个特性,那就是当weak指针指向的对象dealloc时weak指针会自动变为nil。
2.3dealloc
对象的dealloc方法,ARC出现之后一个陌生而又熟悉的函数,将所有的释放工作隐藏在此,下面我们要揭秘dealloc。
在ARC下,编译器会把对象的release方法编译成objc_release, objc_release我们在第1节提到了,objc_release最终的调用函数是objc_object::sidetable_release。
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
//1.取出对象的table
SideTable& table = SideTables()[this];
bool do_dealloc = false;
if (table.trylock()) {
//2.从table.refcnts 取出引用计数计数值
RefcountMap::iterator it = table.refcnts.find(this);
//3.计数值没有找到,就直接将计数值更改为SIDE_TABLE_DEALLOCATING(正在执行dealloc),
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
}
//4.计数值小于 SIDE_TABLE_DEALLOCATING(2),当然也小于SIDE_TABLE_RC_ONE(引用计数单位1,实际的值为4),那么计数值也标记为SIDE_TABLE_DEALLOCATING,准备执行dealloc
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;
}
//5.计数值没有超过SIDE_TABLE_RC_PINNED(就数值最大值),
else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
我们看到objc_object::sidetable_release分了五部分做释放工作。
- 1.取出对象的SideTable。
- 2.以对象作为key从SideTable的refcnts取出引用计数计数值。
- 3.计数值根本没有存,表明引用计数也为零,这个时候讲计数值标记为SIDE_TABLE_DEALLOCATING(正在执行dealloc),准备执行对象dealloc方法。
- 4.计数值小于SIDE_TABLE_DEALLOCATIN,那计数值只能是标记为weak了。那么就给计数值的第1位标记1,即对象处于SIDE_TABLE_DEALLOCATING(正在执行dealloc),准备执行对象dealloc方法。
- 5.计数值大于等于SIDE_TABLE_RC_ONE,并且合法,那么就引用计数减少一。并不执行对象dealloc方法。
那么objc_object::sidetable_release是真正做了release的工作,并且引用计数为0时,调用对象的dealloc方法。
既然这样,我们来看对象的dealloc方法做什么什么事情,我们先自定义一个对象的dealloc。
#import "MyObject.h"
@implementation MyObject
- (void)dealloc
{
}
@end
编译之后
0x10b4106fc <+28>: movq 0x2aad(%rip), %rsi ; (void *)0x000000010b413280: MyObject
0x10b410707 <+39>: movq 0x2a5a(%rip), %rsi ; "dealloc"
0x10b41070e <+46>: movq %rax, %rdi
0x10b410711 <+49>: callq 0x10b410978 ; symbol stub for: objc_msgSendSuper2
0x10b410716 <+54>: addq $0x20, %rsp
0x10b41071a <+58>: popq %rbp
0x10b41071b <+59>: retq
编译器增加了objc_msgSendSuper2,来调用父类的dealloc方法。最终会调用到NSObject的dealloc方法,自定义的dealloc方法做了什么,开发人员自己会知道,但是NSObject做了什么事情,我们需要做一个解释。
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
if (isTaggedPointer()) return;
object_dispose((id)this);
}
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
#if SUPPORT_GC
if (UseGC) {
auto_zone_retain(gc_zone, obj); // gc free expects rc==1
}
#endif
free(obj)
return nil;
}
//objc_destructInstance dealloc的重头戏
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.
//1.如果类存在c++给析构函数,就执行C++析构函数
if (cxx) object_cxxDestruct(obj);
//2.移除和释放通过runtime关联到对象升上的关联对象(一般分类的属性会通过runtime关联进来)
if (assoc) _object_remove_assocations(obj);
//3.清除对象的SideTable的weak_table
if (dealloc) obj->clearDeallocating();
}
return obj;
}
inline void objc_object::clearDeallocating()
{
sidetable_clearDeallocating();
}
void objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
//1.从对象的SideTable的refcnts取出引用计数计数值
RefcountMap::iterator it = table.refcnts.find(this);
//2.计数值存在,并且计数值第0位是1,表明有weak指针指向,这个时候采取调用weak_clear_no_lock清楚weak_table,这块表明计数值第0位的标记是有用的
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
//4.计数值从refcnts移除,可以扔掉,再也不用了。
table.refcnts.erase(it);
}
table.unlock();
}
调用栈入下
dealloc
|__ _objc_rootDealloc
|______ objc_object::rootDealloc()
|_________ object_dispose
|_____________ objc_destructInstance
|_________________object_cxxDestruct
|_________________object_remove_assocations
|_________________sidetable_clearDeallocating
长了点,但是重点在objc_destructInstance和sidetable_clearDeallocating
首先objc_destructInstance有三个重要事情。
- 1.查看对象是否有_cxxDestruct方法,如果有就调用。(object_cxxDestruct一个古怪的东西,后面会讲)
- 2.释放通过runtime增加给对象的关联对象,比如我们在分类中常用的objc_setAssociatedObject方法增加的对象。
- 3.释放对象的weak表,清除完weak表后,weak表中的指针就都会变为nil,weak原理的最后一个谜团完全解开。
其次sidetable_clearDeallocating,是实际的清除weak表的过程,分了3步分
- 1.从对象的SideTable的refcnts取出引用计数计数值
- 2.如果计数值存在,并且计数值的第0位为1,表明对象有weak指针指向,那就调用weak_clear_no_lock清除weak表,我们发现前面说个的计数值的第0位标记是有用的,加快了对象的dealloc.
- 3.最后一步,对象消失了,计数值没有用了,从表中移除。
我们还差一个object_cxxDestruct,一个古怪的方法,到底干了什么?
上面的过程中,我们貌似没有说对象的属性释放的地方,例如
@interface MyObject1 : MyObject
@property (nonatomic, strong) MyObject *next;
@end
MyObject1在释放的时候应该释放next指的对象,会不会object_cxxDestruct就是来做这件事的
我们对如下代码做了debug,
{
MyObject1 *weakObj = [[MyObject1 alloc] init];
weakObj.next = [[MyObject alloc] init];
}
@implementation MyObject1
- (void)dealloc
{
}
@end
发现weakObj指向的对象执行dealloc时堆栈中有一个cxx_destruct的调用
在图中我们发现,weakObj (MyObject1)调用dealloc时,第二行出现了cxx_destruct,并且2-6行的调用栈和我们上面讲的一模一样。黄色框中我们发现有objc_object::sidetable_release的调用,并且MyObject的dealloc也出现了。那么事实出来了,cxx_destruct是用来释放weakObj (MyObject1)中的用属性方式定义的next (MyObject),cxx_destruct用来释放类的属性。
OC通过给对象增加一张weak表来存放所有指向当前对象的weak指针。当对象的dealloc调用时清除该表。当weak指针自己要消失或者值发生变化的时候,也会对该表进行更改
同时ARC下 NSObject dealloc分了三部分来做对象的真正内存释放,其中最后一步就是清楚weak表。来达到weak自动变为nil的操作。
3.__autoreleasing修饰符
__autoreleasing比较简单,就是把对象的持有放入到一个“释放池”中,释放池对对象进行一次持有,当释放池做清理时干掉这些持有。
- (void)test
{
id __autoreleasing arObj = [[NSObject alloc] init];
}
movq 0x2861(%rip), %rsi ; (void *)0x00000001069fee58: NSObject
movq 0x283a(%rip), %rdi ; "alloc"
callq 0x106060b54 ; symbol stub for: objc_msgSend
movq 0x282b(%rip), %rsi ; "init"
callq 0x106060b54 ; symbol stub for: objc_msgSend
callq 0x106060b3c ; symbol stub for: objc_autorelease
编译之后代码比较简单,没有其他的只有一个 objc_autorelease, objc_autorelease干了这么一件事情,代码如下
id objc_autorelease(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
//
inline id objc_object::autorelease()
{
// UseGC is allowed here, but requires hasCustomRR.
assert(!UseGC || ISA()->hasCustomRR());
if (isTaggedPointer()) return (id)this;
if (! ISA()->hasCustomRR()) return rootAutorelease();
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
inline id objc_object::rootAutorelease()
{
assert(!UseGC);
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || *dest == obj);
return obj;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// No pool in place.
assert(!hotPage());
if (obj != POOL_SENTINEL && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
(void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push an autorelease pool boundary if it wasn't already requested.
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
// Push the requested object.
return page->add(obj);
}
调用栈有点长,但不难看懂,按照我们上面说的,肯定有一个容器来存储持有这些autoreleaseing对象,那么上面代码中体现出一个东西,就是AutoreleasePoolPage。autoreleaseFast逻辑很清晰,分三步
- 1.先获取当前的释放池,如果释放池没满,就直接加入进去
- 2.如果满了,并且释放池能获取到,如果当前释放池有子释放池,就将对象加入自释放池,如果没有,就新创建一个。
- 3.如果当前全系统没有释放池,那么就直接创建一个。
上面三步有几个疑问,释放池,AutoreleasePoolPage,子释放池,都是什么意思,系统的释放池是如何建立的。好,我们先来看一下AutoreleasePoolPage定义
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
- AutoreleasePoolPage的定义结构中,对于其他对象的持有使用的是next,AutoreleasePoolPage在创建时申请了一块连续区间,类似一个数组,每一个数组都是一个指针,每当有对象要持有的时候,会调用其add 方法,add方法定义如下
id *add(id obj)
{
assert(!full());
unprotect();
//重点关注
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
//
protect();
return ret;
}
这段代码清晰的说明了持有过程。
2.除了AutoreleasePoolPage的持有以外,系统将释放池设计成一个树状结构,但是这个树状结果比较特殊,每一个AutoreleasePoolPage只有一个子节点。类似于双向链表。
AutoreleasePoolPage
AutoreleasePoolPage
AutoreleasePoolPage
在销毁释放池的时候,会从销毁的释放池开始,销毁起儿子节点,孙子节点,直到没有。
问题来了,为什么要建立一个这样的结构,而且每个释放池清理要清理器子节点。我们看看我们编程的环境,看如下代码
- (void)test
{
id __autoreleasing arObj = [[NSObject alloc] init];
//pool 1
@autoreleasepool {
id __autoreleasing arObj1 = [[NSObject alloc] init];
//pool 2
@autoreleasepool {
id __autoreleasing arObj2 = [[NSObject alloc] init];
}
//pool 3
@autoreleasepool {
id __autoreleasing arObj3 = [[NSObject alloc] init];
}
id __autoreleasing arObj4 = [[NSObject alloc] init];
}
}
可以看到上面代码有三个释放池,每个释放池都有特定作用域,所以我们的需求要达到以下几点
1.每一个释放池管辖特定作用域内的对象。
2.最顶层的@autoreleasepool 出了作用域,内部的@autoreleasepool 也应该出作用域,也就是最顶层释放了,最内部的也得释放。
介于上面两个需求,我们猜测AutoreleasePoolPage分了多个,并且有双向链表结构是为了管控多个作用域的自动释放,并且释放池之间的父子关系和代码作用域的关系一样。
介于此,我们把上面的代码编译一下,编译器干什么事情
movq 0x2981(%rip), %rsi ; (void *)0x000000010c889e58: NSObject
movq 0x295a(%rip), %rdi ; "alloc"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
movq 0x294b(%rip), %rsi ; "init"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
callq 0x10beebb3c ; symbol stub for: objc_autorelease
//pool 1
callq 0x10beebb48 ; symbol stub for: objc_autoreleasePoolPush
0x2943(%rip), %rsi ; (void *)0x000000010c889e58: NSObject
movq 0x291c(%rip), %rdi ; "alloc"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
movq 0x2909(%rip), %rsi ; "init"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
callq 0x10beebb3c ; symbol stub for: objc_autorelease
//pool 2
callq 0x10beebb48 ; symbol stub for: objc_autoreleasePoolPush
movq 0x2901(%rip), %rsi ; (void *)0x000000010c889e58: NSObject
movq 0x28da(%rip), %rdi ; "alloc"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
movq 0x28c7(%rip), %rsi ; "init"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
callq 0x10beebb3c ; symbol stub for: objc_autorelease
//pool 2释放
callq 0x10beebb42 ; symbol stub for: objc_autoreleasePoolPop
//pool 3
callq 0x10beebb48 ; symbol stub for: objc_autoreleasePoolPush
movq 0x28b6(%rip), %rsi ; (void *)0x000000010c889e58: NSObject
movq 0x288f(%rip), %rdi ; "alloc"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
movq 0x287c(%rip), %rsi ; "init"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
callq 0x10beebb3c ; symbol stub for: objc_autorelease
//pool 3释放
callq 0x10beebb42 ; symbol stub for: objc_autoreleasePoolPop
movq 0x2870(%rip), %rax ; (void *)0x000000010c889e58: NSObject
movq 0x2849(%rip), %rsi ; "alloc"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
movq 0x2842(%rip), %rsi ; "init"
callq 0x10beebb54 ; symbol stub for: objc_msgSend
callq 0x10beebb3c ; symbol stub for: objc_autorelease
//pool 1释放
callq 0x10beebb42 ; symbol stub for: objc_autoreleasePoolPop
编译后出现了几个新函数objc_autoreleasePoolPush,objc_autoreleasePoolPop,这两个是成对出现,和@ autoreleasepool的出现顺序一样,objc_autoreleasePoolPush表明新pool的开始,objc_autoreleasePoolPop当前结束。
Ok,最后们再看一下关于objc_autoreleasePoolPush和objc_autoreleasePoolPop的具体实现,
//释放池开始
void * objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
//释放池开始实际调用
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_SENTINEL);
} else {
dest = autoreleaseFast(POOL_SENTINEL);
}
assert(*dest == POOL_SENTINEL);
return dest;
}
//释放池退出调用
void objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}
//释放池退出实际调用
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
// This check is not valid with DebugPoolAllocation off
// after an autorelease with a pool page but no pool in place.
_objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
token);
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
从上面代码可以看出,每个释放池开始时都会调用autoreleaseNewPage创建一个新的,并且成为当前释放池的child (自释放池),当释放池结束,会调用objc_autoreleasePoolPop,起传入参数就是objc_autoreleasePoolPush所创建的释放池对象。 (代码中的其他调用不一一解释了,太多,大家可以去看源码)
最后 我们的app最开头的main函数中
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这个中出现的autoreleasepool为root 释放池。
4. __unsafe_unretained修饰符
__unsafe_unretained这个修饰符我们比较少的用到,因为从表面上看起就是不安全的,因为他不持有对象,对象释放了,再次使用它就会crash。老规矩,编译前后代码如下
- (void)test
{
id __unsafe_unretained usObj = [[NSObject alloc] init];
}
movq 0x2859(%rip), %rsi ; (void *)0x000000010b809e58: NSObject
movq 0x2832(%rip), %rdi ; "alloc"
callq 0x10ae6bb5e ; symbol stub for: objc_msgSend
movq 0x2823(%rip), %rsi ; "init"
callq 0x10ae6bb5e ; symbol stub for: objc_msgSend
callq 0x10ae6bb6a ; symbol stub for: objc_release
可以看出没有任何多余持有过程,表明__unsafe_unretained是不持有。和weak相比,系统也不会对其进行置nil。所以对象消失后,再次访问就要尴尬了。
那么到底有什么用?
相比weak, __unsafe_unretained在自定义的dealloc调用时,是可以访问其地址的,并且不会crash。 所以如果有这样的需求,那__unsafe_unretained可以派上用场。
以上就是基本的四个修饰符的详细解释。
4.盲区
平时开发中,我们往往忽略函数传值和返回值的对象持有方式,现在我们来看看
- (__weak id)test:(__strong id)s1 w1:(__weak id)w1 a:(__autoreleasing id)a1 u:(__unsafe_unretained id)us1
{
return [[NSObject alloc] init];
}
编译后的代码如下
//传值s1,strong
callq 0x1010b0ace ; symbol stub for: objc_storeStrong
//传值w1,weak
callq 0x1010b0aaa ; symbol stub for: objc_initWeak
movq 0x2a99(%rip), %rdi ; (void *)0x0000000101a50e58: NSObject
movq 0x2a5a(%rip), %rsi ; "alloc"
movq 0x1bbb(%rip), %r8 ; (void *)0x000000010169bac0: objc_msgSend
jmp 0x1010b0475 ; <+117> at ViewController.m:51
movq 0x2a44(%rip), %rsi ; "init"
movq 0x1b9d(%rip), %rax ; (void *)0x000000010169bac0: objc_msgSend
jmp 0x1010b0492 ; <+146> at ViewController.m:51
//销毁weak
callq 0x1010b0a9e ; symbol stub for: objc_destroyWeak
//销毁strong
callq 0x1010b0ace ; symbol stub for: objc_storeStrong
//返回值使用autorelease持有
jmp 0x1010b0a98 ; symbol stub for: objc_autoreleaseReturnValue
callq 0x1010b0a9e ; symbol stub for: objc_destroyWeak
movq -0x38(%rbp), %rdi
通过上面的代码看出,函数传值和普通的变量持有没什么大的区别,过程分为两个部分
1.函数调用先对参数从前到后一一做初始化持有。
2.返回值使用autorelease持有后返回。
但我们发现了来个问题
1.传值使用autoreleasing的传值,系统并没有做任何事情,反而和__unsafe_unretained一模一样。
2.使用weak传参,在函数返回之后又来了一次objc_destroyWeak。
这个两个目前还不清楚什么要这么做
总结
本片主要讲了ARC下内存管理的原理,以及常用修饰符的作用原理。
- 1.OC通过给每一个对象增加一个SideTable表,来完成内存管理。SideTable中包含了引用计数就数值。
- 2.SideTable中的weak_table_t作为存储所有指向对象的weak指针,OC通过操作这个weak_table_t来完成weak持有。
- 3.__autoreleasing修饰符,系统增加了释放池,对__autoreleasing的对象进行持有。释放池成双向链表结构。满足不同作用域的需求。
- 4.函数调用前,对函数参数进行一次初始化赋值。函数调用结束,返回值会使用autorelease进行持有。
更新
最新源码地址 https://opensource.apple.com/source/objc4/objc4-706