不管在哪种语言里,内存管理都是个重要概念,要想用一门语言写出内存使用高效且没有bug的代码,就得掌握其内存管理模型的各种细节。本文将详细介绍ARC下objc的内存管理
引用计数
众所周知,Objective-C语言使用引用计数来管理内存。我们可以用开关房间的灯为例来说明引用计数的机制。
假设办公室的照明设备只有一个,上班进入办公的人需要照明,而对于下班离开办公室的人来说,不需要照明。为了解决办公室有人时需要开灯,没人时保持关灯状态这一问题,我们需要:
- 最早进入办公室的人开灯
- 之后进入办公室的人,需要照明
- 离开办公室的人,不需要照明
- 最后离开办公室的人关灯(没有人需要照明)
在Objective-C中,对象相当于办公室照明设备。使用alloc/new/copy/mutableCopy等方法创建对象相当于开灯并需要照明,retain方法持有对象相当于需要照明,release方法释放对象相当于不需要照明,dealloc方法废弃对象相当于关灯。 通过开关房间灯的例子,我们也可以总结出Objc引用计数的几个原则:
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也能持有。
- 不再需要持有的对象,自己可以释放。
- 非自己持有的对象不能释放。
数据结构
SideTables
苹果使用散列表的方案管理对象的引用计数和weak指针,其由SideTables数据结构来实现。其定义如下:
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
虽然SideTables,名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。
为什么不是一个Side Table来实现,而是由多个Side Table共同组成一个Side Tables()这样一个数据结构?
假如只有一张Side Table,相当于我们在内存当中分配的所有对象的引用计数或者说弱引用都放到了一张大表中,此时当我们需要在不同线程对不同对象进行操作时,需要不断的对整个side table加锁,很显然就存在了效率问题。系统为了解决这一问题,引用了分离锁的技术方案。
- 分离锁:可以把内存对象所对应的引用计数表分拆成多个部分,假设分拆成8个,需要对8个表分别加锁,假如对象A在表1中,对象B在表2中,当A和B同时进行引用计数操作时,可以并发操作,但如果只有一张表就只能按顺序操作,分离锁可以提高访问效率.
如何通过一个对象指针,如何快速定位到它属于哪张Side Table表?
SideTables本质是张Hash表,这张Hash表中,可能有64张具体的Side Table,SideTables可以通过对象的内存地址作为key,通过hash函数的一个运算(内存地址一顿操作再来和SideTable表的个数进行取余运算),计算出其对应的SideTable。
通过hash表查找SideTable,对比通过遍历查找对象,可以有效提供效率。同时,由于对象内存地址是均匀分布,当我们通过hash函数来得到对应的SideTable时,可以有效的减少hash冲突。
SideTable
SideTable中包含三个成员,自旋锁,引用计数表,弱引用表。其定义如下:
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
}
自旋锁 Spinlock_t
- 是一种"忙等"的锁,如果当前锁已被其他线程获取,当前线程会不断探测这个锁有没有被释放,如果被释放了,线程就会第一时间去获取这个锁
- 自旋锁适用于轻量访问,锁使用者保持锁时间比较短的情况.
引用计数器 RefcountMap
引用计数器,顾名思义,是用来存储引用计数的,其数据定义如下:
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
三个参数分别代表对象的hash key,引用计数,是否需要在引用计数为0的时候自动释放相应的hash节点,默认为true。
其中存储的引用计数为sizt_t结构,其每个bit代表的含义如下:
- 第一个二进制位(WEAKLY_REFERENCED)表示对象是否有弱引用,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。
- 第二位(DEALLOCATING)表示对象是否正在dealloc
- 其他位(REAL COUNT)存储这个对象实际引用数,所以咱们说的引用计数加一或者减一,实际上是对整个unsigned long加四或者减四
弱引用表weak_table_t
弱应用表是用来保存指定对象的弱引用指针,其定义如下:
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
弱引用表示weak_table_t其实也是一张哈希表,其中weak_entries是一个hash数组,给与一个对象的指针作为key,通过hash函数既可以计算出存储弱引用对象的相关信息weak_entry_t,那么我们再来看weak_entry_t
weak_entry_t
weak_entry_t存储着某个对象的弱引用信息,又是一个结构体。跟weak_table_t类似,里面存储一个hash表weak_referrer_t,弱引用该"对象的指针"的指针。通过weak_referrer_t的操作,可以使该对象的弱引用指针在对象释放后,置为nil。其定义如下:
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
}
主要包含三部分:
- referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
- referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
- inline_referrers 只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。
实现原理
介绍完数据结构,我想大家一定更晕了,引用计数表能理解,弱引用又是啥?我们暂且不表,先回到最先介绍的引用计数原则。里面提到了生成、持有、释放、废弃对象等概念,以及其对应的alloc /retain/release/retainCount/ dealloc等系统方法。本章节将重点介绍这些系统方法的内部实现。
alloc
经过一系列的函数封装及调用,最终调用了C函数calloc,详细可以参考深入浅出ARC(上)
retain
retain的实现可以参考下面代码:
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
//通过当前对象的指针this,到SideTables当中,经过哈希运算,去获取它所属的SideTable
SideTable *table = SideTable::tableForPointer(this);
if (spinlock_trylock(&table->slock)) {
//再通过当前对象的指针this,在SideTable中,也是通过哈希查找,从SideTable当中的引用计数表refcnts中,
//去获取当前对象的引用计数值refcntStorage(无符号long型的值)
size_t& refcntStorage = table->refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//然后再对引用计数值进行+1操作
refcntStorage += SIDE_TABLE_RC_ONE;
}
spinlock_unlock(&table->slock);
return (id)this;
}
return sidetable_retain_slow(table);
}
- 系统经过2次hash查找,先找到对应的SideTable,再找到对应的引用计数size_t
- 由于size_t前2位不是存储引用计数的,所以+1操作实际是通过宏进行了+4的偏移量
release
release比retain稍微复杂的地方就是他需要判断最终是否需要调用dealloc,参考下面代码:
bool
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
//通过当前对象的指针this,到SideTables当中,经过哈希运算,去获取它所属的SideTable
SideTable *table = SideTable::tableForPointer(this);
//默认不需要调用dealloc
bool do_dealloc = false;
if (spinlock_trylock(&table->slock)) {
RefcountMap::iterator it = table->refcnts.find(this);
if (it == table->refcnts.end()) {
//遍历变量是否存在,不存在则标记需要dealloc,并且把标记位设置成正在dealloc
do_dealloc = true;
table->refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
//小于SIDE_TABLE_DEALLOCATING,说明引用数为0,标记需要dealloc
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减1
it->second -= SIDE_TABLE_RC_ONE;
}
spinlock_unlock(&table->slock);
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
return sidetable_release_slow(table, performDealloc);
}
- 系统经过2次hash查找,先找到对应的SideTable,再找到对应的引用计数size_t
- 如果找不到对象,标记为需要dealloc
- 如果小于SIDE_TABLE_DEALLOCATING,说明引用数为0,标记为需要dealloc
- 否则引用计数减1,实际通过宏进行偏移
- 如果需要dealloc,则调用dealloc
retainCount
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable *table = SideTable::tableForPointer(this);
size_t refcnt_result = 1;
spinlock_lock(&table->slock);
RefcountMap::iterator it = table->refcnts.find(this);
if (it != table->refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
spinlock_unlock(&table->slock);
return refcnt_result;
}
刚刚新alloc出来的一个对象,在引用计数表中,是没有这个对象相关联的key-value映射,这个值读出来的it -> second应该为0,然后由于局部变量refcnt_result是1,所以此时,只经过alloc调用的对象,调用它的retainCount,获取到的就是1
dealloc
通过源码,不难发现NSObject执行dealloc时调用_objc_rootDealloc继而调用object_dispose随后调用objc_destructInstance方法,前几步都是条件判断和简单的跳转,最后的这个函数如下:
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;
}
- 判断是否有C++相关的内容,或者当前对象是否采用了ARC(hasCxxDtor)?,如果有的话则调用object_cxxDestruct()
- 判断是否有关联对象,如果有的话,则调用_object_remove_assocations()
- 调用clearDeallocating()函数,清理弱引用。
自动引用计数ARC
自动引用计数(Automatic Reference Counting)是指内存管理中引用采用自动计数的计数,与之对应的MRC(Manual Reference Counting)。虽然现在oc开发者已经很少使用MRC了,但是ARC中有相当一大部分是由MRC的机制和原理组成的。
使用规则
在ARC机制中,编译器负责进行内存管理,开发者无需再次键入retain或者release。但是需要遵循以下规则:
- 不能使用retain/release/retainCount/autorelease。
- 不能使用NSAllocateObject/NSDeallocateObject。
- 必须遵守内存管理的命名规则。
- 不能显示的调用dealloc,比如[super dealloc]。
- 用@autorelease代替NSAutoreleasePool。
- 不能使用NSZone。
- 对象类型变量不能作为C语言结构体成员(可以用__unsafe_unretain修饰之后使用)
- 不能显示转换id和void*(可以通过_bridge,_bridge_retained,__bridge_transfer)。
- 新增weak, strong属性关键字
循环引用
这里,我们再次提到弱引用(weak)。那么弱引用到底是什么,他的作用是什么?他又是怎么实现的?
在介绍弱引用前,我们先简单介绍strong修饰符。它是id类型和对象类型默认的所有权修饰符。表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。
看起来strong修饰符就能够完美地进行内存管理。但遗憾的是,引用计数内存管理必然会引起循环引用问题。以下个例子为例:
@interface Test: NSObject
@property(nonatomic, strong) object;
@implementation Test
- (id) init {
.....
}
@end
{
//循环引用
id test0 = [[Test alloc ] init];
id test1 = [[Test alloc ] init];
[test0 setObject:test1];
[test1 setObject:test0];
}
若此时对象test0的obj指向对象test1,同时对象test1中的obj指向对象test0,即循环引用。产生循环引用时,因为test0、test1对象的引用计数都不为0。因此造成对象无法被释放。发生内存泄漏。
除此之外循环引用还有下面2种情况:
- 自循环引用:假如有一个对象,内部强持有它的成员变量obj,而此时我们给obj赋值为原对象
- 多循环引用:假如类中有对象1...对象N,每个对象中都强持有一个obj,若每个对象的obj都指向下个对象,就产生了多循环引用
那么如何破除循环引用呢?
通常我们破除循环引用的方法有2个
- 使用弱引用,避免产生相互循环引用
- 在合适的时机手动断环
手动断环暂且不表,弱引用顾名思义,与强引用恰好相反,弱引用不能持有对象,所以在超出变量作用域时,对象即被释放。同时弱引用还有另外有点,当对象被废弃时,弱引用将被自动置为nil。
弱引用
那么弱引用是怎么实现的呢?先看下面代码:
NSObject *obj = [[NSObject alloc] init];
__weak NSObject *obj1 = obj;
经编译器转换后,变成如下实现:
id obj1;
objc_initWeak(&obj1,obj);
再来看objc_initWeak的实现:
id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效
// 无效对象直接导致指针释放
if (!newObj) {
*location = nil;
return nil;
}
// 这里传递了三个 bool 数值
// 使用 template 进行常量参数传递是为了优化性能
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
可以看出,该函数先是做了一些异常判断,然后调用storeWeak函数。其代码如下:
// HaveOld: true - 变量有值
// false - 需要被及时清理,当前值可能为 nil
// HaveNew: true - 需要被分配的新值,当前值可能为 nil
// false - 不需要分配新值
// CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
// false - 用 nil 替代存储
template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
// 该过程用来更新弱引用指针的指向
// 初始化 previouslyInitializedClass 指针
Class previouslyInitializedClass = nil;
id oldObj;
// 声明两个 SideTable
// ① 新旧散列创建
SideTable *oldTable;
SideTable *newTable;
// 获得新值和旧值的锁存位置(用地址作为唯一标示)
// 通过地址来建立索引标志,防止桶重复
// 下面指向的操作会改变旧值
retry:
if (HaveOld) {
// 更改指针,获得以 oldObj 为索引所存储的值地址
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (HaveNew) {
// 更改新值指针,获得以 newObj 为索引所存储的值地址
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 加锁操作,防止多线程中竞争冲突
SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);
// 避免线程冲突重处理
// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
if (HaveOld && *location != oldObj) {
SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
goto retry;
}
// 防止弱引用间死锁
// 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
if (HaveNew && newObj) {
// 获得新对象的 isa 指针
Class cls = newObj->getIsa();
// 判断 isa 非空且已经初始化
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized()) {
// 解锁
SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
// 对其 isa 指针进行初始化
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// 如果该类已经完成执行 +initialize 方法是最理想情况
// 如果该类 +initialize 在线程中
// 例如 +initialize 正在调用 storeWeak 方法
// 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记
previouslyInitializedClass = cls;
// 重新尝试
goto retry;
}
}
// ② 清除旧值
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);
// 如果弱引用被释放 weak_register_no_lock 方法返回 nil
// 在引用计数表中设置若引用标记位
if (newObj && !newObj->isTaggedPointer()) {
// 弱引用位初始化操作
// 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
newObj->setWeaklyReferenced_nolock();
}
// 之前不要设置 location 对象,这里需要更改指针指向
*location = (id)newObj;
}
else {
// 没有新值,则无需更改
}
SideTable::unlockTwo<HaveOld, HaveNew>(oldTable, newTable);
return (id)newObj;
}
- haveOld是否有旧值,例如weak指针obj1在指向objc之前,指向另外一个对象test
- haveNew是否有新值,weak指针被指向objc
- 根据上述2个标志位,获取对应新旧SideTable
- 确保新对象初始化完成
- 如果有旧值,通过weak_unregister_no_lock清除旧值的weak指针绑定
- 如果有新值,通过weak_register_no_lock完成新的weak指针绑定
weak_unregister_no_lock实现如下:
#define WEAK_INLINE_COUNT 4
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id) {
// 在入口方法中,传入了 weak_table 弱引用表,referent_id 旧对象以及 referent_id 旧对象对应的地址
// 用指针去访问 oldObj 和 *location
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
// 如果其对象为 nil,无需取消注册
if (!referent) return;
// weak_entry_for_referent 根据首对象查找 weak_entry
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 通过地址来解除引用关联
remove_referrer(entry, referrer);
bool empty = true;
// 检测 out_of_line 位的情况
// 检测 num_refs 位的情况
if (entry->out_of_line && entry->num_refs != 0) {
empty = false;
}
else {
// 将引用表中记录为空
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
// 从弱引用的 zone 表中删除
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
// 这里不会设置 *referrer = nil,因为 objc_storeWeak() 函数会需要该指针
}
- 通过oldObj和weakTable查找到对应weak_entry_t。
- 通过weak指针地址通来解除弱引用关联
- 如果解除后weak_entry_t为空,移除对应entry
这一步与上一步相反,weak_register_no_lock完成weak指针和新对象的绑定,其实现如下:
id weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating) {
// 在入口方法中,传入了 weak_table 弱引用表,referent_id 旧对象以及 referent_id 旧对象对应的地址
// 用指针去访问 oldObj 和 *location
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
// 检测对象是否生效、以及是否使用了 tagged pointer 技术
if (!referent || referent->isTaggedPointer()) return referent_id;
// 保证引用对象是否有效
// hasCustomRR 方法检查类(包括其父类)中是否含有默认的方法
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
// 检查 dealloc 状态
deallocating = referent->rootIsDeallocating();
}
else {
// 会返回 referent 的 SEL_allowsWeakReference 方法的地址
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
// 由于 dealloc 导致 crash ,并输出日志
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
// 记录并存储对应引用表 weak_entry
weak_entry_t *entry;
// 对于给定的弱引用查询 weak_table
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 增加弱引用表于附加对象上
append_referrer(entry, referrer);
}
else {
// 自行创建弱引用表
weak_entry_t new_entry;
new_entry.referent = referent;
new_entry.out_of_line = 0;
new_entry.inline_referrers[0] = referrer;
for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) {
new_entry.inline_referrers[i] = nil;
}
// 如果给定的弱引用表满容,进行自增长
weak_grow_maybe(weak_table);
// 向对象添加弱引用表关联,不进行检查直接修改指针指向
weak_entry_insert(weak_table, &new_entry);
}
// 这里不会设置 *referrer = nil,因为 objc_storeWeak() 函数会需要该指针
return referent_id;
}
当一个对象被废弃/释放之后,weak变量是如何处理的?在dealloc的实现中,如果存在weak引用时,会在clearDeallocating()函数中调用weak_clear_no_lock()函数清除弱引用。其实现如下:
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
/**
通过新声明的局部变量referent,在弱引用表weak_table去查找它的对应的弱引用数组entry。
weak_entry_for_referent函数:添加weak变量的时候也遇到过。
通过被废弃对象的指针,经过哈希算法的计算,求出弱引用数组对应的数组索引位置。
通过索引返回给调用方当前对象所对应的弱引用数组。
*/
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
/**
如果弱引用变量的个数小于4的话,就取inline_referrers,反之取referrers,
总之referrersd所取到的,就是最终当前对象对应的所有弱引用指针的数组列表
*/
if (entry->out_of_line) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
// *referrer:当前对象曾被修饰过的所有的弱引用指针
objc_object **referrer = referrers[i];
if (referrer) {
//如果 *referrer弱引用指针代表的地址就是被废弃的地址referent的话,就置为nil
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
weak_clear_no_lock内部会根据当前对象指针查找弱引用表,把当前对象相对应的弱引用都拿出来,是一个数组, 然后遍历数组,遍历所有的弱引用指针,如果弱引用指针代表的地址就是被废弃的地址referent的话,就置为nil。
自动释放池
最后,我们来看一段代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@"autorelease"];
NSLog(@"%@",str); //Console: autorelease
Console输出了str的值,那么问题来了,str是在什么时候被释放的?是在viewDidLoad被调完成作用域释放时吗?
答案是在每一次runloop循环将要结束时,会对前一次创建的AutoreleasePool进行pop操作,同时会push进来一个新的AutoreleasePool,所以在viewDidLoad中所创建的str对象,是在当次runloop将要结束的时候,调用AutoreleasePoolPage的pop方法中释放的
当然,我们也可以手动干预Autorelease对象的释放时机:
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"autorelease"];
}
NSLog(@"%@",str); //Console: null
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
NSString *str = [NSString stringWithFormat:@"autorelease"];
objc_autoreleasePoolPop(context);
查看runtime源码,可以看到objc_autoreleasePoolPush()和objc_autoreleasePoolPop实现如下:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
也就是说这两个函数都是通过AutoreleasePoolPage的push和pop来实现的,那么我们再来看其实现:
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
bool lessThanHalfFull() {
return (next - begin() < (end() - begin()) / 2);
}
...
}
可以看出:
- AutoreleasePoolPage对象通过双向链表的形式连接在一起,parent 指向父结点,child 指向子结点
- next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
- thread 指向当前线程;说明了,AutoreleasePoolPage和线程一一对应的。
AutoreleasePoolPage中Push方法的内部实现
- 每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象(POOL_BOUNDARY),值为0(也就是个nil)
- 然后将next指针指向下一个可入栈的位置,实际上每次进行AutoreleasePool的代码块创建的时候,相当于不断的在栈中去插入哨兵对象
[obj autorelease]方法实现
当我们需要入栈一个autorelease对象时,首先会判断当前next指针是否指向栈顶,若没有指向栈顶,则直接把对象添加到当前栈的next位置。假如当前next已经位于栈顶,那么当前AutoreleasePoolPage就没办法添加新的autorelease对象了,就会再创建一个AutoreleasePoolPage对象。第一个AutoreleasePoolPage对象的child指向第二个AutoreleasePoolPage对象,第二个AutoreleasePoolPage对象的parent指向第一个AutoreleasePoolPage对象。如下图所示:
AutoreleasePoolPage中Pop方法的内部实现
objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
- 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
autoreleasepool嵌套
autoreleasePool的多层嵌套调用就是多次插入哨兵对象,当我们每次进行autoreleasePool代码块创建的时候,系统就会为我们进行哨兵对象的插入
回到开始的问题,str是什么时候被释放的?
iOS在主线程的Runloop中注册了2个Observer
- 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
- 第2个Observer
- 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
- 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
参考文章:
Objective-c高级编程-ios与OS X多线程和内存管理
深入浅出ARC(上)
黑幕背后的Autorelease
weak 弱引用的实现方式