原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 六、SideTables 散列表中的弱引用表 weak_entry_t 的源代码解析
- 1、weak_entry_t
- 2、添加weak变量
- 3、清除weak变量
- 4、strong与Unretained
- 七、自动释放池 Autoreleasepool
- 1、自动释放池
- 2、autorelease pool本质上是一个stack
- 3、手动调用autoreleasepool
- 4、子线程AutoRelease对象何时释放
- Demo
- 参考文献
六、SideTables 散列表中的弱引用表 weak_entry_t 的源代码解析
全局的SideTables()方法获取的其实是一个StripedMap<SideTable>*类型的对象,该对象内部创建了一个最大容量为StripeCount(在iOS真机中StripeCount=8;iOS模拟器中StripeCount=64)的静态数组,所有的对象都通过indexForPointer函数映射到[0,StripeCount)的索引位置进行存储。
struct SideTable {
spinlock_t slock; //保障操作安全的锁
RefcountMap refcnts;//保存对应的引用计数:当isa中extra_rc不足以保存时,使用散列表保存refcnts.find(obj)
weak_table_t weak_table;//保存weak_table_t
...
};
1、weak_entry_t
weak_table_t是全局的弱引用表,所有的弱引用都在该表中进行存储,使用不定类型对象的地址作为key,用weak_entry_t类型结构体对象作为 value。
struct weak_table_t {
weak_entry_t *weak_entries; //保存了所有指向指定对象的weak指针
size_t num_entries; // weak对象的存储空间大小
uintptr_t mask;// 参与判断引用计数辅助量
uintptr_t max_hash_displacement;// hash key 最大偏移值:hash冲撞时最大尝试次数,用于优化搜索算法
};
weak_entry_t是用来存储具体某一对象的所有弱引用指针。当对象的弱引用指针小于4个时,使用静态数组inline_referrers进行保存,同时out_of_line_ness = 0;。当对象弱引用指针大于4个时,使用二维数组referrers保存,out_of_line_ness = 2;。
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
//当弱引用指针个数大于WEAK_INLINE_COUNT时,使用二维指针进行存储
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
//当弱引用指针个数小于WEAK_INLINE_COUNT时,使用一位数组进行存储
struct {
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
//判断当前是否是离线存储
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
//重载运算符=
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
//第一个弱引用指针使用该方法存储
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
a、查找weak_entry_t
找到对应对象的存放位置,需要处理hash冲突,如果存在hash冲突会往下一个查找,查找到返回对应weak_entry_t,当超过了最大的冲突处理次数后,说明没有查找到,就会返回nil。
static weak_entry_t *weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
// hash算法
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
// 找到对应对象的存放位置,需要处理hash冲突,如果存在hash冲突会往下一个查找,直到找到空位,这个就是开放地址法处理hash冲突
while (weak_table->weak_entries[index].referent != referent)
{
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;// 冲突处理次数加1
// 当超过了最大的冲突处理次数后,说明没有查找到,就会返回nil
if (hash_displacement > weak_table->max_hash_displacement)
{
return nil;
}
}
// 查找到返回对应weak_entry_t
return &weak_table->weak_entries[index];
}
b、判断weak_table_t是否需要扩容
默认容量大小初始值是64,若weaktable当前的使用量超过最大容量的3/4,就在原来的容量基础上*2,否则不做任何处理。
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// 超过最大容量的3/4
if (weak_table->num_entries >= old_size * 3 / 4)
{
// 在原来的容量基础上*2,默认初始值是64
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
c、插入新的weak_entry_t
通过hash算法找到为nil的位置,存储weak_entry_t。
- 使用
hash_pointer(new_entry->referent) & (weak_table->mask)获取起始索引 - 循环查找
weak_entries中为空的位置 - 在
index位置保存new_entry并让num_entries进行自增 - 保存哈希碰撞最大的尝试次数,在查找时可以减少搜索次数
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
// 跟查找一样的hash算法
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 找到为nil的位置
while (weak_entries[index].referent != nil)
{
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;// 当前的最大冲突次数
}
weak_entries[index] = *new_entry;// 存储weak_entry_t
weak_table->num_entries++;// 已存储位置数+1
if (hash_displacement > weak_table->max_hash_displacement)
{
// 当前冲突次数比max_hash_displacement大,则赋值给max_hash_displacement
weak_table->max_hash_displacement = hash_displacement;
}
}
d、移除weak_entry_t
分别从静态和动态数组中移除即可。判断是否是弱引用是否是离线存储,如果使用非离线存储则遍历inline_referrers查找old_referrer,查找到则置空。离线存储机制,使用w_hash_pointer(old_referrer) & (entry->mask)获取起始索引,遍历entry->referrers,找到之后置空,并让entry->num_refs自减。
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
// 从静态数组中移除
if (! entry->out_of_line())
{
for (size_t i = 0; i < WEAK_INLINE_COUNT; I++)
{
if (entry->inline_referrers[i] == old_referrer)
{
entry->inline_referrers[i] = nil;
}
}
}
// 从动态数组中移除
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != old_referrer)
{
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
}
entry->referrers[index] = nil;
entry->num_refs--;
}
2、添加weak变量
添加的位置是通过哈希算法进行查找,如果查找到的位置已经有了当前对象的弱引用数组,就把新的weak变量添加到该数组,如果没有则创建新数组,添加weak变量到第一个,其余初始化为nil。
a、调用objc_initWeak()
runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。objc_initWeak方法中的两个参数location是指weak指针,newObj是 weak指针将要指向的对象。
id objc_initWeak(id *location, id newObj)
{
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
b、调用storeWeak()
objc_initWeak函数会调用 objc_storeWeak()函数, objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表。这里要先进行haveOld判断,也就是如果该指针有指向的旧值,则从旧表中移除weak地址值,然后在新表中插入weak指针的地址,成功返回newObj,否则返回nil。
static id storeWeak(id *location, objc_object *newObj)
{
// 散列表里面有旧的weak对象则将其移除掉
if (haveOld)
{
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
if (haveNew)
{
// 在新表中插入weak指针的地址,成功返回newObj,否则返回nil
newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
// Tagged Pointer是苹果用来优化内存的
if (newObj && !newObj->isTaggedPointer())
{
// 在引用计数表中标记有弱引用指向,当引用计数为0时,触发移除相对应的weak指针
newObj->setWeaklyReferenced_nolock();
}
// 将weak指针指向新的对象
*location = (id)newObj;
}
}
c、调用weak_register_no_lock()
weak_register_no_lock()的功能是判断是否存在弱引用weak_entry_t,有则在对应的数组中插入当前需要插入的weak指针,没有则新建一个weak_entry_t再将数据插入到弱引用表中。
append_referrer用来在weak_entry_t中添加新的弱引用指针。当前没有使用离线存储,遍历内部静态数组inline_referrers,有空余的位置则直接保存new_referrer,否则开辟新的空间指针new_referrers,并将原始静态数组的元素复制到新开辟空间中的对应位置,然后重置entry相关属性。此时entry->num_refs=WEAK_INLINE_COUNT是大于TABLE_SIZE(entry) * 3/4)。判断离线存储的实际使用量(num_refs)是否大于空间总量的(TABLESIZE(entry)),如果为真则需要扩容并添加新的弱引用指针,直接返回grow_refs_and_insert,否则正常存储。
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
// 运用TaggedPointer计数,无需维护weak_table_t弱引用表
if (!referent || referent->isTaggedPointer()) return referent_id;
// 从散列表的weak_table中创建weak_entry_t
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent)))
{
// 插入weak指针,存储弱引用对象
append_referrer(entry, referrer);
}
// 没有weak_entry_t
else
{
// 创建新的weak_entry_t
weak_entry_t new_entry(referent, referrer);
// 判断weak_table是否需要扩容
weak_grow_maybe(weak_table);
// 插入到散列表中的弱引用表中
weak_entry_insert(weak_table, &new_entry);
}
return referent_id;
}
3、清除weak变量
a、dealloc的流程图
__weak Person *weakPerson = person;
举个例子,假如我们的person对象已经被释放掉了,那么就需要告诉对象weakPerson一声,防止产生野指针导致程序崩溃,而clearDeallocating函数所做的事情就是把很多类似weakPerson这样的弱引用全部都置为nil。具体weak是如何实现的,这里给了一个表。
weak_table
从图中知道有个全局表叫SideTables,它里面维护了所有对象的引用计数,还有弱引用对象。当RefcountMap表中某个对象的引用计数为0时,系统会调用此类的dealloc方法,再调用其父类的dealloc,沿着继承链一直调用到NSObject的dealloc方法,最终走到objc_destructInstance函数里,主要操作四个:释放实例变量、移除关联属性、把弱引用置为nil、释放自己self。
dealloc实现
在调用dealloc之后,会经过 _objc_rootDealloc()到rootDealloc(),然后判断是否可以释放?判断释放的条件比较关键:
-
nonpointer_isa:判断这个isa指针类型,是否为非指针型的isa -
weakly_referenced:是否有弱引用 -
has_assoc:是否有关联对象 -
has_cxx_dtor:判断是否有C++实现 -
has_sideTable_rc:引用计数是否在sideTable中有存储
如果有一个满足条件,调用 object_disponse(),再开始释放,object_disponse()的实现如下:
objc_dispose()的实现
先判断是否有C++实现,然后判断是否有关联对象,如果没有c++,也没有关联对象,则直接调用clearDeallocating(),否则分别调用object_cxxDestruct()和_object_remove_assocaations来释放。在这里系统内部实现清除了分类里定义的关联对象实例。
clearDeallocating实现
接下来在调用 clearDeallocating的方法中,会调用sidetable_calearDeallocationg() 和weak_clear_no_lock()两个方法,作用是将指向该对象的弱引用指针置为nil,这就是为什么我们不需要在dealloc中将指向他的弱引用指针置为nil的原因,接下来会调用table.refcnts.erase(),来进行引用计数的擦除操作,然后结束流程。
b、dealloc的源码解析
❶ dealloc方法
销毁对象这块最终需要调用到NSObject类中的dealloc方法。
// 释放对象
- (void)dealloc
{
_objc_rootDealloc(self);
}
❷ rootDealloc方法
若当前对象是标签指针则直接返回,因为标签指针不指向任何对象,只是一个保存了标签和数据的假指针。
若当前对象使用nonpointer计数,且不包含弱引用指针、关联对象、析构函数,以及全局散列表中不包含对象引用计数数据时,直接释放对象空间,否则继续执行object_dispose。从这里可以大致看出对象释放时最起码要处理指向对象的弱指针引用、动态关联到对象的属性、对象的析构函数以及存储在全局散列表中引用计数等。
inline void objc_object::rootDealloc()
{
if (isTaggedPointer()) return;
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.getClass(false)->hasCxxDtor() &&
!isa.has_sidetable_rc))
{
free(this);// 没有以上内容则直接释放
}
else
{
object_dispose((id)this);
}
}
❸ object_dispose方法
id object_dispose(id obj)
{
//若obj不存在直接返回
if (!obj) return nil;
//调用objc_destructInstance
objc_destructInstance(obj);
//释放对象空间
free(obj);
return nil;
}
❹ objc_destructInstance方法
void *objc_destructInstance(id obj)
{
if (obj)
{
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// 释放 C++ 的实例变量
if (cxx) object_cxxDestruct(obj);
// 移除关联属性
if (assoc) _object_remove_assocations(obj);
// 将弱引用置为nil
obj->clearDeallocating();
}
return obj;
}
❺ clearDeallocating方法
inline void objc_object::clearDeallocating()
{
// 从散列表中清除弱引用表
sidetable_clearDeallocating();
}
❻ sidetable_clearDeallocating方法
void objc_object::sidetable_clearDeallocating()
{
// 取得散列表
SideTable& table = SideTables()[this];
// 添加线程锁
table.lock();
// 获取迭代器
RefcountMap::iterator it = table.refcnts.find(this);
// 判断引用计数的数值是否到了最后,如果到了说明该对象即将释放
if (it != table.refcnts.end())
{
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED)
{
// 进行清除弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
// 散列表中的引用计数表移除该对象
table.refcnts.erase(it);
}
table.unlock();
}
❼ weak_clear_no_lock方法
判断弱引用表中是否存在referent对应的weak_entry_t结构,否则说明该对象不存在弱引用指针直接返回。判断弱引用表使用静态数组存储还是使用离线存储,并获取存储空间首地址referrers和弱引用指针个数count。遍历存储空间查找指向referent的弱引用指针并置空。
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
// 遍历弱引用数组逐个置空指向referent的弱引用指针
for (size_t i = 0; i < count; ++i)
{
objc_object **referrer = referrers[I];
if (referrer)
{
if (*referrer == referent)// 当前弱指针指向对象referent
{
// 这就是为什么weak对象被释放后不会引起崩溃问题,因为这里将其置为了nil
*referrer = nil;
}
}
}
// entry不再有存在的价值将其从弱引用计数表中移除
weak_entry_remove(weak_table, entry);
}
c、数组 weak_entries
在weak_table里有个数组weak_entries,数组里元素都是结构体weak_entry_t,这结构体里面有被引用的对象referent,还有引用数组referrers和inline_referrers。需要注意的是,不是所有的对象都具有结构体weak_entry_t的,只有当某个对象具有弱引用时,才会给这对象创建一个weak_entry_t,并把它添加到数组weak_entries中去。同理当一个对象变得没有弱引用时,会从数组weak_entries中删去它对应的weak_entry_t。我们知道一个对象可能有多个弱引用,比如:
__weak Person *weakPerson1 = person;
__weak Person *weakPerson2 = person;
此时weakPerson1和weakPerson2都会放到weak_entry_t中的referrers数组或者inline_referrers数组中去,二者区别主要是看数组长度大小超过4了没有,超过4,则放到referrers中,否则放到inline_referrers中,此时对应的referent是person。
d、头文件 objc-weak.h
这里说个非常重要的头文件objc-weak.h,它专门处理OC中对象的弱引用问题,里面有几个核心方法:
-
weak_register_no_lock:给指定对象添加一个弱引用,当我们执行
_weak ClassA *objB = objA类似代码时,会触发NSObject的objc_initWeak方法,最终会调用到weak_register_no_lock方法 - weak_unregister_no_lock:移除指定对象的一个弱引用
- weak_is_registered_no_lock:判断指定对象是否存在弱引用
-
weak_clear_no_lock:清除指定对象所有弱引用,上述
clearDeallocating里最终调用的就是此方法
e、弱引用清除函数 weak_unregister_no_lock
对象dealloc后,会调用弱引用清除函数weak_unregister_no_lock,根据该对象查找弱引用表,遍历弱引用数组所有指针,分别置为nil。
- 在
weak_table中查找对应的entry - 从
entry中移除referrer,并判断移除之后entry是否为空 - 若移除之后当前
entry为空,则从weak_table中移除entry
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{
// 获取被引用对象
objc_object *referent = (objc_object *)referent_id;
// 强转referrer_id为二级指针
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
if (!referent) return;
// 查找 referent 对象对应的 entry
if ((entry = weak_entry_for_referent(weak_table, referent)))
{
// 如果弱引用指针存在则从entry移除referrer
remove_referrer(entry, referrer);
// 判断移除referrer之后entry是否为空
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0)
{
empty = false; // entry 非空
}
else
{
for (size_t i = 0; i < WEAK_INLINE_COUNT; I++)
{
// 否则循环查找 entry 数组, 如果 4 个位置中有一个非空
if (entry->inline_referrers[I])
{
empty = false; // entry 非空
break;
}
}
}
if (empty)// 如果移除referrer之后entry为空则从表中移除entry
{
// 从 weak_table 中移除该条 entry
weak_entry_remove(weak_table, entry);
}
}
}
其中从 weak_table 中移除该条 entry的weak_entry_remove()方法源代码如下。
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// 如果是静态数组,直接释放静态数组
if (entry->out_of_line()) free(entry->referrers);
// 释放entry
bzero(entry, sizeof(*entry));
// 对应的已存数量-1
weak_table->num_entries--;
// 这里会查看下weakTable的容量是否需要减容
weak_compact_maybe(weak_table);
}
上面讲到过扩容,相对应的这里使用到的减容的方法 weak_compact_maybe()源代码如下。
static void weak_compact_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// 当容量超过1024且是利用率不及1/16的时候,减容
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries)
{
// 在原有的基础上除以8
weak_resize(weak_table, old_size / 8);
}
}
4、strong与Unretained
Strong的流程
typedef enum
{
objc_ivar_memoryUnknown, // unknown / unknown
objc_ivar_memoryStrong, // direct access / objc_storeStrong
objc_ivar_memoryWeak, // objc_loadWeak[Retained] / objc_storeWeak
objc_ivar_memoryUnretained // direct access / direct access
} objc_ivar_memory_management_t;
case objc_ivar_memoryStrong: objc_storeStrong(location, value);
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev)
{
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
Unretained为什么不安全
不再是指针传递,而直接是值传递,倘若value对象置为nil,location却不知道,这就导致内存泄露。
case objc_ivar_memoryUnretained: *location = value; break;
七、自动释放池 Autoreleasepool
1、自动释放池
a、@autoreleasepool
用来管理自动释放池。release消息马上将引用计数减1,而使用autorelease 的对象的引用计数并不变化,而是向内存释放池中添加 一 条记录,会延迟到内存释放池周期到后,向池中所有对象发送 release 消息,引用计数才减 1。 当引用计数为 0时,对象所占用的内存才被释放 。
应用程序入口文件 main.m,代码被包裹在@autoreleasepool {... }之间,这是池的作用范围,默认是整个应用,释放默认在程序结束。
int main(int argc, char * argv[])
{
@autoreleasepool
{
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
编译的时候,这段代码会被转换成:
{
__AtAutoreleasePool __autoreleasepool;
}
其中,出现的结构体__AtAutoreleasePool为:
struct __AtAutoreleasePool
{
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这表明,我们的main函数实际执行了:
int main(int argc, const char * argv[])
{
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
@autoreleasepool只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。
b、Autorelease Pool 的主要结构
每一个autorelease pool都是由一系列的AutoreleasePoolPage组成。
class AutoreleasePoolPage
{
magic_t const magic; //完整性检验
id *next;
pthread_t const thread; //保存当前的线程
AutoreleasePoolPage * const parent; //双线链表使用 指向父节点
AutoreleasePoolPage *child; //双向链表使用 指向子结点
uint32_t const depth;
uint32_t hiwat;
};
可见,自动释放池的AutoreleasePoolPage是以双向链表的结构连接起来的,当一张表存满了就可以根据双向链表寻找到上一张或者下一张表。自动释放池和线程是一一对应的关系。在自动释放池的内存中,AutoreleasePoolPage被以栈结构存储起来。
@autoreleasepool {}被转换为一个 __AtAutoreleasePool结构体。这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这表明,我们的main函数在实际工作时其实是这样的。@autoreleasepool 只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
下面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push和 pop 的封装。
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES
自动释放池中的 AutoreleasePoolPage 是以双向链表的形式连接起来的,parent 和child就是用来构造双向链表的指针。
如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下。其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。begin() 和 end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。magic 用于对当前 AutoreleasePoolPage 完整性的校验。thread保存了当前页所在的线程。
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中。
POOL_SENTINEL 只是 nil 的别名。在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。下面的 atautoreleasepoolobj 就是一个 POOL_SENTINEL。
#define POOL_SENTINEL nil
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL。
c、什么对象会加入Autoreleasepool中
- 使用
alloc、new、copy、mutableCopy的方法进行初始化时,由系统管理对象,在适当的位置release - 使用
array会自动将返回值的对象注册到Autoreleasepool -
__weak修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool中 -
id的指针或对象的指针,在没有显示指定时会被注册到Autoleasepool中
d、Autorelease对象的释放时机
autorelease释放对象的依据是Runloop,简单说,runloop就是iOS中的消息循环机制,当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。
2、autorelease pool本质上是一个stack
上文有提到autorelease pool,一旦一个对象被标记为autorelease,则该对象会被放到iOS的一个池:autorelease pool,其实这个autorelease pool本质上是一个stack,扔到autorelease pool中的对象等价于入栈。
a、objc_autoreleasePoolPush() 的入栈流程图
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
-
push就是在page中插入一个哨兵对象,代表这些属于要一起release的对象 - 如果
page满了,则创建新的page,和老的page关联起来,将对象指针压栈 - 多层嵌套就是多次插入哨兵对象
objc_autoreleasePoolPush()
objc_autoreleasePoolPush 方法会调用 AutoreleasePoolPage 的类方法 push,在这里会进入一个比较关键的方法 autoreleaseFast,并传入哨兵对象 POOL_SENTINEL。
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
static inline void *push() {
return autoreleaseFast(POOL_SENTINEL);
}
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);
}
}
上述方法分三种情况选择不同的代码执行,最后的都会调用 page->add(obj) 将对象添加到自动释放池中。hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
- 有
hotPage并且当前page不满- 调用
page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
- 调用
- 有
hotPage并且当前page已满- 调用
autoreleaseFullPage初始化一个新的页 - 调用
page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
- 调用
- 无
hotPage- 调用
autoreleaseNoPage创建一个hotPage - 调用
page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
- 调用
page->add 将对象添加到自动释放池页中。这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针。
id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}
autoreleaseFullPage 会在当前的 hotPage 已满的时候调用。它会从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage,否则使用构造器传入 parent 创建一个新的 AutoreleasePoolPage。在查找到一个可以使用的 AutoreleasePoolPage 之后,会将该页面标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
如果当前内存中不存在 hotPage,就会调用 autoreleaseNoPage 方法初始化一个 AutoreleasePoolPage。既然当前内存中不存在 AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。初始化之后,将当前页标记为 hotPage,然后会先向这个 page 中添加一个 POOL_SENTINEL 对象,来确保在 pop 调用的时候,不会出现异常。最后,将 obj 添加到自动释放池中。
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
return page->add(obj);
}
b、objc_autoreleasePoolPop() 的出栈流程图
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
- 根据传入的哨兵对象找到对应位置
- 给上次
push操作之后添加的对象依次发送release消息 - 回退
next指针到正确位置
objc_autoreleasePoolPop()
让我们重新回到对 objc_autoreleasePoolPop 方法的分析,也就是 AutoreleasePoolPage::pop 方法的调用。该静态方法总共做了三件事情:
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
- 使用
pageForPointer获取当前token所在的AutoreleasePoolPage - 调用
releaseUntil方法释放栈中的对象,直到stop - 调用
child 的 kill方法
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
pageForPointer 方法主要是通过内存地址的操作,获取当前指针所在页的首地址,将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的。
releaseUntil 方法的实现,用一个 while 循环持续释放 AutoreleasePoolPage 中的内容,直到 next 指向了 stop 。使用 memset 将内存的内容设置成 SCRIBBLE,然后使用 objc_release 释放对象。
void releaseUntil(id *stop) {
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_SENTINEL) {
objc_release(obj);
}
}
setHotPage(this);
}
到这里,没有分析的方法就只剩下 kill 了,而它会将当前页面以及子页面全部删除。
void kill() {
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
我们已经对自动释放池生命周期有一个比较好的了解,最后需要了解的话题就是 autorelease 方法的实现,先来看一下方法的调用栈。在 autorelease 方法的调用栈中,最终都会调用上面提到的 [autoreleaseFast]方法,将当前对象加到 AutoreleasePoolPage 中。
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)
- 自动释放池是由
AutoreleasePoolPage以双向链表的方式实现的 - 当对象调用
autorelease方法时,会将对象加入AutoreleasePoolPage的栈中 - 调用
AutoreleasePoolPage::pop方法会向栈中的对象发送release消息Object 编译后的样子
c、压入对象
push 方法:压入
static inline void *push()
{
id *dest;
// 是否已经初始化过
if (slowpath(DebugPoolAllocation))// 是否开启新的页面
{
// 每个自动释放池从一个新的池页开始
dest = autoreleaseNewPage(POOL_BOUNDARY);
}
else// 不开启新页面则直接压入进去
{
// POOL_BOUNDARY是哨兵对象,可以看成边界
dest = autoreleaseFast(POOL_BOUNDARY);
}
return dest;
}
autoreleaseNewPage方法:开启新的页面
id *autoreleaseNewPage(id obj)
{
// 找到page的最终子page并创建新page,再将obj加入其中
AutoreleasePoolPage *page = hotPage();
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
autoreleaseFast 方法:添加对象
static inline id *autoreleaseFast(id obj)
{
// 获取当前page(双向链表) ,默认加到最近激活的page中而不是再新开辟一个page
AutoreleasePoolPage *page = hotPage();
// 判断page是否存在并且是否存储空间已经存满
if (page && !page->full())
{
// 没有存满就添加当前对象
return page->add(obj);
}
else if (page)
{
// 存满就创建一个新的FullPage并将对象加入其中
return autoreleaseFullPage(obj, page);
}
else
{
// page不存在就需要开辟空间去创建根page
return autoreleaseNoPage(obj);
}
}
autoreleaseFullPage方法:存满就创建一个新的FullPage
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
do
{
// child指向自己,不断找下去
if (page->child) page = page->child;
// 开启一个新的page
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);// 在新page中加入对象
}
autoreleaseNoPage方法:创建根页面
id *autoreleaseNoPage(id obj)
{
// 创建根页面
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
// 激活根页面
setHotPage(page);
// 添加边界线
if (pushExtraBoundary)
{
page->add(POOL_BOUNDARY);
}
// 将对象加入到根page中
return page->add(obj);
}
d、弹出对象
popPage 方法:弹出对象
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
// 释放page中的每一个obj
page->releaseUntil(stop);
// 先获取到当前页面,再找到当前页面的父页面,接着杀死当前页面,再将父页面激活
page->kill();
setHotPage(parent);
// 如果父页面不存在则直接杀死当前页面,激活页面置为nil
page->kill();
setHotPage(nil);
}
releaseUntil 方法:释放对象
void releaseUntil(id *stop)
{
// 遍历下一个对象直到遇到停止为至
while (this->next != stop)
{
// 获取当前page
AutoreleasePoolPage *page = hotPage();
// 找到插入的对象
AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
// 对象不是哨兵就对其进行释放
if (obj != POOL_BOUNDARY)
{
for (int i = 0; i < count + 1; I++)
{
objc_release(obj);
}
}
}
}
kill方法:杀死页面
void kill()
{
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;// 获取当前页面的子页面
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;// 获取当前页面的父页面
if (page)
{
page->unprotect();// 解锁
page->child = nil;// 将子页面置空
page->protect();// 加锁
}
delete deathptr;
} while (deathptr != this);
}
e、嵌套使用
@autoreleasepool
{ //Push : atautoreleasepoolobj = objc_autoreleasePoolPush() POOL_BOUNDARY 的地址~
Man *man = [Man object];
_objc_autoreleasePoolPrint();
@autoreleasepool
{//Push : atautoreleasepoolobj = objc_autoreleasePoolPush() POOL_BOUNDARY 的地址~
_objc_autoreleasePoolPrint();
Man *man1 = man;
//[man autorelease]
@autoreleasepool
{//Push : atautoreleasepoolobj = objc_autoreleasePoolPush() POOL_BOUNDARY 的地址~
_objc_autoreleasePoolPrint();
Man *man2 = man;
// NSLog(@"current Count : %d",_objc_rootRetainCount(man));
} //objc_autoreleasePoolPop(POOL_BOUNDARY的地址~);
}//objc_autoreleasePoolPop(POOL_BOUNDARY的地址~);
}//objc_autoreleasePoolPop(POOL_BOUNDARY的地址~);
3、手动调用autoreleasepool
a、释放掉大量临时变量
既然由runloop来决定对象释放时机而不是作用域,那么在一个{}内使用循环大量创建对象就有可能带来内存上的问题,大量对象会被创建而没有及时释放,这时候就需要靠我们人工的干预autorelease的释放了。手动添加的是大括号结束的时候释放。
我们把需要及时释放掉的代码块放入我们生成的autorelease pool中,结束后清空这个自定义的pool,主动地让pool清空掉,从而达到及时释放内存的目的。以上述图片处理的例子为例,优化如下。
// 创建一个自动释放池
@autoreleasePool
{
for (int i = 0; i <= 1000; i ++)
{
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"PNG"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];
}
}
// 将自动释放池内存释放,它会同时释放掉上面代码中产生的临时变量image
有个值得注意的地方,在集合对象的enumerateObjectsUsingBlock方法中,如果我们临时创建了很多内存较大的对象,是不需要手动在block内部添加自动释放池的,因为系统已经为我们手动添加了自动释放池了。我们想要让对象的引用计数提前变为0,就必须要手动添加自动释放池让其提前释放,这里的手动添加操作由系统来实现了。
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UIImage *image = ...
}];
b、测试自动释放池的容量
❶ 注意需要关闭ARC并添加autorelease才能打印出对象
@autoreleasepool
{
for (int i= 0; i<503; I++)
{
//NSObject *objc = [[NSObject alloc] init];
NSObject *objc = [[[NSObject alloc] init] autorelease];
}
_objc_autoreleasePoolPrint();
}
❷ 打印所有信息
void _objc_autoreleasePoolPrint(void)
{
AutoreleasePoolPage::printAll();
}
❸ 未关闭ARC并且未添加autorelease
objc[6177]: ##############
objc[6177]: AUTORELEASE POOLS for thread 0x10ba83e00
objc[6177]: 0 releases pending.
objc[6177]: [0x1] ................ PAGE (placeholder)
objc[6177]: [0x1] ################ POOL (placeholder)
objc[6177]: ##############
objc[6222]: ##############
objc[6222]: AUTORELEASE POOLS for thread 0x1113b6e00
objc[6222]: 504 releases pending.
objc[6222]: [0x7fe83200e000] ................ PAGE (hot) (cold)
objc[6222]: [0x7fe83200e038] ################ POOL 0x7fe83200e038
objc[6222]: [0x7fe83200e040] 0x600002f1c010 NSObject
objc[6222]: [0x7fe83200e048] 0x600002f1c020 NSObject
...
objc[6222]: [0x7fe83200eff0] 0x600002f1df70 NSObject
objc[6222]: ##############
❹ 一页表所能容纳的大小为 4096 / PAGE_SIZE,所以大约容纳503个对象后就满了。倘若将503更改为506则会由于溢出开启新的页面
objc[6571]: ##############
objc[6571]: AUTORELEASE POOLS for thread 0x10da4ae00
objc[6571]: 507 releases pending.
objc[6571]: [0x7fd6ec80c000] ................ PAGE (full) (cold)
objc[6571]: [0x7fd6ec80c038] ################ POOL 0x7fd6ec80c038 // 边界符
objc[6571]: [0x7fd6ec80c040] 0x600003424000 NSObject
...
objc[6571]: [0x7fd6ec80cff8] 0x600003426060 NSObject
objc[6571]: [0x7fd6ec80e000] ................ PAGE (hot) // 出到边界符的时候就将页面进行销毁
objc[6571]: [0x7fd6ec80e038] 0x600003426070 NSObject
objc[6571]: [0x7fd6ec80e040] 0x600003426080 NSObject
objc[6571]: ##############
4、子线程AutoRelease对象何时释放
在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool。那么子线程呢?子线程的runloop都需要手动开启,那么子线程中使用autorelease对象会产生内存泄漏吗,如果不会又是什么时候释放呢?
a、子线程的autoreleasepool如何被创建?
得知如果当前线程没有AutorelesepoolPage的话,代码执行顺序为autorelease -> autoreleaseFast -> autoreleaseNoPage。在autoreleaseNoPage方法中,会创建一个hotPage,然后调用page->add(obj)。也就是说即使这个线程没有AutorelesepoolPage,使用了autorelease对象时也会new一个AutoreleasepoolPage出来管理autorelese对象,不用担心内存泄漏。子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中通过懒加载创建一个出来。
static inline id autorelease(id obj)
{
id *dest __unused = autoreleaseFast(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);
}
}
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
return page->add(obj);
}
b、子线程的autoreleasepool何时被释放?
明确了何时创建autoreleasepool以后就自然而然的有下一个问题,这个autoreleasepool何时清空?对于这个问题,这里使用watchpoint set variable命令来观察。首先是一个最简单的场景,创建一个子线程:
__weak id obj;
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread) toTarget:self withObject:nil];
使用一个weak指针观察子线程中的autorelease对象,子线程中执行的任务。
- (void)createAndConfigObserverInSecondaryThread
{
__autoreleasing id test = [NSObject new];
NSLog(@"obj = %@", test);
obj = test;
[[NSThread currentThread] setName:@"test runloop thread"];
NSLog(@"thread ending");
}
在obj = test处设置断点使用watchpoint set variable obj命令观察obj,可以看到obj在释放时的方法调用栈是这样的。
obj在释放时的方法调用栈
通过这个调用栈可以看到释放的时机在_pthread_exit。然后执行到AutorelepoolPage的tls_dealloc方法。这个方法如下(简化版的):
static void tls_dealloc(void *p)
{
// pop all of the pools
if (!page->empty()) pop(page->begin());
// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}
在这找到了if (!page->empty()) pop(page->begin());这句关键代码。再往上看一点,在_pthread_exit时会执行下面这个函数:
void _pthread_tsd_cleanup(pthread_t self)
{
for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++)
{
pthread_key_t k;
for (k = __pthread_tsd_start; k <= self->max_tsd_key; k++)
{
_pthread_tsd_cleanup_key(self, k);
}
}
}
也就是说thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_delloc中,执行了pop操作。就算插入后没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool,即子线程的runloop在退出的时候会自动执行pop操作。当子线程被释放掉的时候,AutoreleasePoolPage作为子线程中的资源也会被一起清空掉。
c、子线程加入runloop的效果
上述这个例子中的线程并没有加入runloop,只是一个一次性的线程。现在给这个线程加入runloop来看看效果会是怎么样的。对于runloop,我们知道runloop一定要有source才能保证run起来以后不立即结束,而source有三种,custom source,port source,timer。先加一个timer尝试下。
- (void)createAndConfigObserverInSecondaryThread
{
[[NSThread currentThread] setName:@"test runloop thread"];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
// 用于监控runloop的状态
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopAllActivities,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopRef cfrunloop = [loop getCFRunLoop];
if (observer)
{
CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(testAction) userInfo:nil repeats:YES];
[loop run];
NSLog(@"thread ending");
}
相应测试代码为:
- (void)testAction
{
__autoreleasing id test = [NSObject new];
obj = test;
NSLog(@"obj = %@", obj);
}
在testAction()中加上watchpoint断点,观察obj的释放时机。
obj的释放时机
可以看到释放的时机在CFRunloopRunSpecific中,也就是runloop切换状态的时候。即使是我们自定义的source,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。
Demo
Demo在我的Github上,欢迎下载。
BasicsDemo











网友评论