本文章基于 objc4-725 进行测试.
objc4 的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.
本篇文章主要分析 AutoreleasePool 销毁相关操作的函数.
AutoreleasePoolPage 类的成员函数
AutoreleasePoolPage 类的静态函数和成员函数众多, 有些函数没有贴出源码, 只写了内部逻辑, 所以需要结合源码来看.
-
pop
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) { //如果 token 为空池标志
if (hotPage()) { //如果有 hotPage, 即池非空
pop(coldPage()->begin()); //将整个自动释放池销毁
} else {
setHotPage(nil); //没有 hotPage, 即为空池, 设置 hotPage 为 nil
}
return;
}
page = pageForPointer(token); //根据 token 找到所在的 节点
stop = (id *)token; //token 转换给 stop
if (*stop != POOL_BOUNDARY) { //如果 stop 中存储的不是哨兵节点
if (stop == page->begin() && !page->parent) {
//存在自动释放池的第一个节点存储的第一个对象不是哨兵对象的情况, 有两种情况导致:
//1. 顶层池呗是否, 但留下了第一个节点(有待深挖)
//2. 没有自动释放池的 autorelease 对象(有待深挖)
} else {
//非自动释放池的第一个节点, stop 存储的也不是哨兵对象的情况
return badPop(token); //调用错误情况下的 badPop()
}
}
if (PrintPoolHiwat) printHiwat(); //如果需要打印 hiwat, 则打印
page->releaseUntil(stop); //将自动释放池中 stop 地址之后的所有对象释放掉
if (...) {
//这一段代码都是调试用代码
} else if (page->child) { //如果 page 有 child 节点
if (page->lessThanHalfFull()) { //如果 page 已占用空间少于一半
page->child->kill(); //kill 掉 page 的 child 节点
} else if (page->child->child) { //如果 page 的占用空间已经大于一半, 并且 page 的 child 节点有 child 节点
page->child->child->kill(); //kill 掉 child 节点的 child 节点
}
}
}
pop() 函数的主要作用是根据自动释放池状态以及传入的 token 参数来决定合适的释放方案, 如果传入的 token 是空池标识, 则需要确保销毁整个自动释放池; 如果 token 和自动释放池状态冲突, 则调用 badPop(); 如果释放操作是正常的, 则使用 releaseUntil() 方法来释放 stop 之后的 autorelease 对象, 释放完成后如果 hotPage 使用量过半, 则预留下一级节点, 从下下一级的节点开始 kill, 这样可以节省创建新节点的时间, 如果 hotPage 的使用量未过半, 则从下一级节点开始 kill, 并不预留节点, 这样可以节省空间.
-
coldPage
通过
while (result->parent) {
result = result->parent;
result->fastcheck();
}
这种形式, 一直找到自动释放池的第一个节点.
-
pageForPointer
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE; //转换为十进制数的 p 余上 4096
assert(offset >= sizeof(AutoreleasePoolPage)); //如果余数小于 AutoreleasePoolPage 的大小则抛出异常
result = (AutoreleasePoolPage *)(p - offset); //十进制数 p 减掉刚刚得到的余数 offset, 结果转换为AutoreleasePoolPage * 类型指针
result->fastcheck(); //根据配置进行 check
return result;
}
由于为 AutoreleasePoolPage 对象分配的地址都是按 4096 对齐的, 也就是说 AutoreleasePoolPage 对象所处的地址都是 4096 的倍数, 所以 token 转换为十进制数时, 对 4096 取余, 就能得到 token 地址对 AutoreleasePoolPage 对象地址的偏移量. 又因为 AutoreleasePoolPage 对象本身的大小是 56, 所以如果 token 对 4096 取余的结果如果小于 56 就是错误的, 此时会抛出异常. 否则 token 地址减去偏移量, 就是 AutoreleasePoolPage 对象的地址, 转换为 AutoreleasePoolPage * 类型的指针, 就是该 token 所处的 page 节点.
-
badPop
static void badPop(void *token)
{
// 对于旧的 SDK 来说, 这个错误并不是致命的
if (DebugPoolAllocation || sdkIsAtLeast(10_12, 10_0, 10_0, 3_0, 2_0)) {
//对于开启 pool 内存分配的 debug 模式, 以及最新 SDK 的情况, 调用到 badPop 是错误的
_objc_fatal(...); //输出一系列错误信息
}
// 旧 SDK 下, Bad pop 会记录一次日志
static bool complained = false; //这个静态变量确保下面的 crush log 只写入一次
if (!complained) {
complained = true;
_objc_inform_now_and_on_crash(...); //输出一系列信息到 crash log 里, 但不会触发 crash
}
objc_autoreleasePoolInvalid(token); //摧毁包含 token 的自动释放池
}
首先这个函数正常情况下是调用不到的, 只有使用旧 SDK 的时候有可能会发生. 一旦发生 badPop 时, 会记录下错误日志, 并销毁该自动释放池.
-
releaseUntil 和 releaseAll
void releaseAll()
{
releaseUntil(begin()); //直接调用 releaseUntil, 传入 begin()
}
void releaseUntil(id *stop)
{
//这里没有使用递归, 防止发生栈溢出
while (this->next != stop) { //一直循环到 next 指针指向 stop 为止
AutoreleasePoolPage *page = hotPage(); //取出 hotPage
//接手的开发者认为这里也可以用 if 来代替 while, 但是找不到证据证明自己, 所以他留下了这么一句注释:
//fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) { //从节点 page 开始, 向前找到第一个非空节点
page = page->parent; //page 非空的话, 就向 page 的 parent 节点查找
setHotPage(page); //把新的 page 节点设置为 HotPage
}
page->unprotect(); //如果需要的话, 解除 page 的内存锁定
id obj = *--page->next; //先将 next 指针向前移位, 然后再取出移位后地址中的值
memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); //将 next 指向的内存清空为SCRIBBLE
page->protect(); //如果需要的话, 设置内存锁定
if (obj != POOL_BOUNDARY) { //如果取出的对象不是哨兵对象
objc_release(obj); //给取出来的对象进行一次 release 操作
}
}
setHotPage(this); //将本节点设置为 hotPage
#if DEBUG
// 调试模式下, 检查刚刚被释放的 page 节点是否都为空
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
自动释放池销毁对象中最重要的一环, 调用者是用 pageForPointer() 找到的, token 所在的 page 节点, 参数为 token. 这个函数主要操作流程就是, 从 hotPage 开始, 使用 next 指针遍历存储在节点里的 autorelease 对象列表, 对每个对象进行一次 release 操作, 并且把 next 指向的指针清空, 如果 hotPage 里面的对象全部清空, 则继续循环向前取 parent 并继续用 next 指针遍历 parent, 一直到 next 指针指向的地址为 token 为止. 因为 token 就在 this 里面, 所以这个时候的 hotPage 应该是 this.
-
lessThanHalfFull
bool lessThanHalfFull() {
return (next - begin() < (end() - begin()) / 2);
}
next - begin() 是已经使用的字节数, end() - begin() 是一共可以用来存储 autorelease 对象的字节数, 这里判断使用量是否过半.
-
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; //child 置空
page->protect(); //如果需要的话, 设置内存锁定
}
delete deathptr; //回收刚刚保留的节点, 重载 delete, 内部调用 free
} while (deathptr != this);
}
自动释放池中需要 release 的对象都已操作完成, 此时 hotPage 之后的 page 节点都已经清空了, 需要把这些节点的内存都回收, 操作方案就是从最后一个节点, 遍历到调用者节点, 挨个回收.
-
~AutoreleasePoolPage
AutoreleasePoolPage 的析构函数, 内部都是检查类的函数, 判断销毁 AutoreleasePoolPage 之前, pop() 操作是否正确执行完成, 如果出现意外则会直接抛出异常.
至此 AutoreleasePool 的销毁操作已经全部完成.
值得注意的就是自动释放池销毁时, 仅仅是为相应的 autorelease 对象调用 release 方法, 并不会直接销毁该对象, 该对象是否销毁还是要看它本身的引用计数. 另外 autorelease 对象加入到自动释放池时不会调用 retain 方法, 但加入到自动释放池时不会判重, 所以对一个对象调用多次 autorelease 方法的话, 会重复加入自动释放池, 最后销毁时会多次 release, 引发 crash.