AutoreleasePool 的实现机制 (四)

本文章基于 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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容