iOS - autoreleasePool

AutoreleasePool引入

请添加图片描述

我们看一下objc_autoreleasePoolPush和objc_autoreleasePoolPop这两个函数的实现
请添加图片描述

他们都调用了AutoreleasePoolPage这个函数,我们看一下这个函数的实现

AutoreleasePoolPage的结构实现

//当只有一个池时,空的\池\占位符存储在TLS中
    
    //已推送,且从未包含任何对象。这样可以节省内存
    
    //当顶层(即libdispatch)推送和弹出池,但
    
    //永远不要使用它们。
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
    // 哨兵对象
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    // AutoreleasePoolPage的大小,通过宏定义,可以看到是4096字节
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
    //4096
#endif
    static size_t const COUNT = SIZE / sizeof(id);
	//定义内存大小  4096
    magic_t const magic; // 16字节
    // 一个AutoreleasePoolPage中会存储多个对象
    // next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处)
    id *next; // 8字节 通过begin()方法初始化:
    // 保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)
    pthread_t const thread; // 8字节 当前pool所处的线程
    // AutoreleasePoolPage是以双向链表的形式连接
    // 前一个节点
    
    AutoreleasePoolPage * const parent; // 8字节
    // 后一个节点
    AutoreleasePoolPage *child; // 8字节
    uint32_t const depth; // 4字节 page的深度,首次为0,以后每次初始化一个page都加1。
    uint32_t hiwat; // 4字节 这个字段是high water的缩写,这个字段用来计算pool中最多存放的对象个数。在每次执行pop()的时候,会更新一下这个字段。
    // SIZE-sizeof(*this) bytes of contents follow
    //固定的内容占56字节,之外的可以放入page中的对象地址


AutoreleasePoolPage结构

在AutoreleasePoolPage中总共有4096个字节,有56个字节来存储结构体信息(如下图所示)其余用来存储添加到Page中的对象地址

在这里插入图片描述

  • 结构体第一个成员变量是magic,用来检查这个节点是否已经被初始化了。(同isa一样)
  • thread保存当前也所在的线程
  • depth表示page的深度,首次为0,每个page的大小都是4096字节(16进制0x1000),每次初始化一个page,depth都加一。它并不影响整个自动释放池的实现,也不在关键方法的调用栈中。
  • parent和child两个指针,其实是双向链表。在autoreleasePoolPage内部,各个autoreleasePoolPage之间的连接方式是双向链表
  • next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如上图所示移动到下一个为空的内存地址中
  • 关于pool与page的关系的理解
    在这里插入图片描述

请添加图片描述

POOL_BOUNDARY(哨兵对象)

 #define POOL_BOUNDARY nil

POOL_BOUNDARY 只是 nil 的别名,POOL_BOUNDARY直译过来就是POOL的边界。它的作用是隔开page中的对象。并不是每次push与pop之间存进的对象都刚好占满一个page,可能会不满,可能会超过。因此这个POOL_BOUNDARY帮助我们分隔每个@autoreleasepool块之间的对象(其实这句话我不太理解)
在每个自动释放池初始化调用objc_autoreleasePoolPush的时候,都会把一个POOL_SENTINELpush到自动释放池的栈顶,并且返回这个哨兵对象

而当方法objc_autoreleasePoolPop调用时,就会想自动释放池中的对象发送release消息,直到第一个POOL_SENTINEL

在这里插入图片描述
只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY

AutoreleasePool流程

先前我们说到过AutoreleasePool主要调用objc_autoreleasePoolPush和objc_autoreleasePoolPop这两个函数

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

objc_autoreleasePoolPush方法

我们先看一下objc_autoreleasePoolPush中push的实现

static inline void *push() 
    {
        id *dest;
        // POOL_BOUNDARY就是nil
        // 首先将一个哨兵对象插入到栈顶
        if (DebugPoolAllocation) {
            // 区别调试模式
            // 调试模式下将新建一个链表节点,并将一个哨兵对象添加到链表栈中
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
            //POOL_BOUNDARY就是nil
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }


我们来看一下autoreleaseFast这个方法

    static inline id *autoreleaseFast(id obj)
    {
        // hotPage就是当前正在使用的AutoreleasePoolPage
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            // 有hotPage且hotPage不满,将对象添加到hotPage中
            return page->add(obj);
        } else if (page) {
            // 有hotPage但是hotPage已满
            // 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
            return autoreleaseFullPage(obj, page);
        } else {
            // 无hotPage
            // 使用autoreleaseNoPage创建一个hotPage,并将对象添加到新创建的page中
            return autoreleaseNoPage(obj);
        }
    }


上述方法分三种情况选择不同的代码执行:

    • 有 hotPage 并且当前 page 不满
      调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
    • 有 hotPage 并且当前 page 已满
      调用 autoreleaseFullPage 初始化一个新的页
      调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
    • 无 hotPage
      调用 autoreleaseNoPage 创建一个 hotPage
      调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
      最后的都会调用 page->add(obj) 将对象添加到自动释放池中。
  • hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。

add添加对象
简化版:

id *add(id obj) {
        id *ret = next;
        *next++ = obj;
        return ret;
    }
//这其实就是一个压栈操作,将对象加入AutoreleasePoolPage,然后移动栈顶指针

autoreleaseNoPage无hotPage

    id *autoreleaseNoPage(id obj) {
        bool pushExtraBoundary = false;
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        // 将初始化的AutoreleasePoolPage设置成hotPage
        setHotPage(page);
        // Push the requested object or pool.
        // 将对象添加到AutoreleasePoolPage中
        return page->add(obj);
    }

初始化一个新的AutoreleasePoolPage,将当前也标记为hotPage,然后先想这个page中添加一个哨兵对象。最后将obj添加到这个page中

autoreleaseFullPage当前hotPage对象

    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);
    }


如果page有child(AutoreleasePoolPage未满),就用它来存放,如果没有,就使用构造器传入Parent新建一个来存放

autoreleaseFast参数

上面我们可以看到dest = autoreleaseFast(POOL_BOUNDARY);
也就是说push调用的autoreleaseFast参数只有POOL_BOUNDARY

此外请注意,push操作是不带参数的,等于就是初始化的时候调用一次。
那么真正压入对象的方法是什么呢
我们先写一个将对象压入自动释放池的操作

int main(int argc, const char * argv[]) {
        id __strong obj0 = [[NSObject alloc] init];//生成对象A
        id __autoreleasing obj2 = obj0;
    return 0;
}

通过查看汇编发现它其实调用的是objc_retainAutorelease方法,之后层层调用发现调用的是

static inline id autorelease(id obj)
    {
        printf("static inline id autorelease%p\n", obj);
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }


会帮我们把对象当成参数传进去,然后都调用fast方法

objc_autoreleasePoolPop方法

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();
        }
    }
}


可以看到pop是一个静态方法,其总共做了三件事

  • 使用pageForPointer获取当前token所在的AutoreleasePoolPage
  • 调用releaseUntil方法释放栈中的对象(是个while循环)、直到stop[token],stop就是传递的参数,一般为哨兵对象
  • 调用child的kill方法
    系统根据当前页的不同状态kill掉不同child的页面
    releaseUntil把page里的对象进行了释放,但是page本身也会占据很多空间,要通过kill()来处理
    如果当前page小于一半满,则把当前页的所有孩子都杀掉,否则,留下一个孩子,从孙子开始杀。Apple假设,当前page一半都没满,说明已经够了,把接下来的全kill,如果超过一半,就认为下一页还有存在的必要,所以从孙子开始杀
    这样子我认为是为了节约创建page操作,牺牲内存来提升性能。

token

  • token是指向该pool的POOL_BOUNDARY指针
  • token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
  • 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY

总结

总结一下:

  • 调用push方法会将一个POOL_BOUNDARY(哨兵对象)入栈,并且返回其存放的内存地址,然后压入对象
  • 调用pop方法时传入一个POOL_BOUNDARY(哨兵对象)的内存地址,会从最后一个入栈的对象开始发送release消息,释放栈中的对象,直到遇到这个POOL_BOUNDARY
  • id *next指向了下一个能存放autorelease对象地址的区域

小tips:

  • 关于哨兵对象(POOL_BOUNDARY)和next指针:
    next指针只有一个,永远指向下一个能存放autoreleasepool的地址,而哨兵对象可以有很多个,每个autoreleasePool都对应一个哨兵对象,标示这个autoreleasePool对象从哪里开始存。

  • next和child:
    next指向下一个能存放autoreleasepool的地址,child是autoreleasePoolPage的参数,指向下一个page。
    问题:

  • autoreleasePoolPage与RunLoop的关系
    引言:
    在这里插入图片描述
    答:
    RunLoop和AutoReleasePool是通过线程的方式一一对应的
    在非手动添加Autorelease pool下,Autorelease对象是在当前runloop进入休眠等待前被释放的
    当一个runloop在不停的循环工作,那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠):而去BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池,那么这两个方法来销毁要释放的对象,所以我们根本不需要担心Autorelease的内存管理问题

  • 休眠具体在干嘛runloop
    (后续补充)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值