iOS——Autoreleasepool底层原理

什么是autoreleasepool

顾名思义,autoreleasepool也称为自动释放池,类似于C语言中的自动变量,我们可以将对象加入到autoreleasepool中,当其超出其作用域时,调用release释放该对象。
在之前探究ARC实现时,我们可以发现ARC将非自己持有的对象加入到了autoreleasepool。
在这里插入图片描述
下面深入了解一下autoreleasepool的结构

autoreleasepool的结构

在objc中查看源码可以发现最后会走到:
在这里插入图片描述
上面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push 和 pop 的封装。下面看看这个AutoreleasePoolPage。

AutoreleasePoolPage

在objc中点开其源码:

// MARK: - AutoReleasepool class
class AutoreleasePoolPage 
{
    
    
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   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
    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
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    // 校验完整性
    magic_t const magic;
    
    // next指针指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
    id *next;
    
    // 指向当前线程
    pthread_t const thread;
    
    // 父节点
    AutoreleasePoolPage * const parent;
    
    // 子节点
    AutoreleasePoolPage *child;
    
    // 节点深度
    uint32_t const depth;
    uint32_t hiwat;

    // SIZE-sizeof(*this) bytes of contents follow

    static void * operator new(size_t size) {
    
    
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    static void operator delete(void * p) {
    
    
        return free(p);
    }

这个数据结构很熟悉,原来Auto releasePool的底层是由一系列的 AutoreleasePoolPage 组成的
(并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000))

#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

结构如下所示:
在这里插入图片描述
是一个双向链表,parent 和 child 就是用来构造双向链表的指针。

在官方文档对于AutoreleasePool的解释中,我们可以看到下面几个概念,在下面的源码用得上。


Autorelease pool implementation
 
- A thread's autorelease pool is a stack of pointers. 
线程的自动释放池是指针的堆栈
 
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
每个指针都是要释放的对象,或者是POOL_BOUNDARY,它是自动释放池的边界。
 
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象。
 
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. 
堆栈分为两个双向链接的页面列表。根据需要添加和删除页面。
 
- Thread-local storage points to the hot page, where newly autoreleased objects are stored. 
线程本地存储指向热页面,该页面存储新自动释放的对象。

1、自动释放池 是一个 关于指针的栈结构

2、其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界)
pool_boundary:POOL_BOUNDARY:nil的宏定义,替代之前的哨兵对象POOL_SENTINEL,在自动释放池创建时,在objc_autoreleasePoolPush中将其推入自动释放池中。在调用objc_autoreleasePoolPop时,会将池中对象按顺序释放,直至遇到最近一个POOL_BOUNDARY时停止。

3、自动释放池是一个页的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中提及过,即类的继承链)

4、一些名词

  • hotPage:是当前正在使用的page,操作都是在hotPage上完成,一般处于链表末端或者倒数第二个位置。存储在 TLS 中,可以理解为一个每个线程共享一个自动释放池链表。

    扫描二维码关注公众号,回复: 14690997 查看本文章
  • coldPage:位于链表头部的page,可能同时为hotPage

  • EMPTY_POOL_PLACEHOLDER:当自动释放池中没有推入过任何对象时,这个时候推入一个POOL_BOUNDARY,会先将EMPTY_POOL_PLACEHOLDER存储在 TLS 中作为标识符,并且此次并不推入POOL_BOUNDARY。等再次有对象被推入自动释放池时,检查在 TLS 中取出该标识符,这个时候再推入POOL_BOUNDARY

  • next:指向AutoreleasePoolPage指向栈顶空位的指针,每次加入新的元素都会往上移动。

autoreleasePool中的栈

如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:
在这里插入图片描述
其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。
begin() 和 end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:
在这里插入图片描述
在栈中release加入的元素时候,就会根据哨兵也就是pool_boundary来作为边界Pop。
当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL:
在这里插入图片描述

autoreleasePool的实现源码

objc_autoreleasePoolPush

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

它调用 AutoreleasePoolPage 的类方法 push,也非常简单:

// push 返回的是next指针指向的obj(是一个released obj)
    static inline void *push() 
    {
    
    
        id *dest;
        if (DebugPoolAllocation) {
    
    
            
            // 每一个autoreleasepool对象开始于一个新的autoreleasepoolpage
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
    
    
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

objc_autoreleasePoolPush方法其实就是向自动释放池推入一个POOL_BOUNDARY,作为该autoreleasepool的起点。autoreleaseFast方法的具体逻辑将在后面分析autorelease方法时再进行分析。

autorelease

下面看加入自动释放池方法autorelease的代码实现:

static inline id autorelease(id obj) {
    
    
        id *dest __unused = autoreleaseFast(obj);
        return obj;
}

可以看到这里把对象作为参数调用了autoreleaseFast

 // 创建一个page逻辑
    static inline id *autoreleaseFast(id obj)
    {
    
    
        // hotPage可以理解为正在使用的AutoreleasePoolPage
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
    
    
            // 有 hotPage 且不满 直接加入栈中
            return page->add(obj);
        } else if (page) {
    
    
            // hotPage 已满 先创建一个 Page 后,加入新 Page 中
            return autoreleaseFullPage(obj, page);
        } else {
    
    
            // 没有 hotPage 直接新建一个 Page,并加入 Page 中
            return autoreleaseNoPage(obj);
        }
    }

这段代码的逻辑:

  • 如果HotPage存在且未满,则直接推入hotPage。
  • 如果HotPage存在且已经满,则调用autoreleaseFullPage,去初始化一个新的页,并推入
  • 如果不存在HotPage,则调用autoreleaseNoPage,创建一个hotPage,推入hotPage。

autoreleaseFullPage(当前 hotPage 已满)

 // MARK: - 创建一个new page
    static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
    
    
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);
        // 找到一个未满的 page , 未找到则新建一个 page ,设置成 hotPage
        do {
    
    
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

该方法逻辑如下:

  • 查看hotPage是否有后继节点(page),如果有直接使用后继节点。
  • 如果没有后继节点,则新建一个AutoreleasePoolPage。
  • 将对象加入获取到的page,并将其设置为hotPage,其实就是存入 TLS 中共享。

autoreleaseNoPage(没有 hotPage)

// autoreleasepool中没有page
    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
    
    
        // "No page" could mean no pool has been pushed
        // or an empty placeholder pool has been pushed and has no contents yet
        // 执行 No page 表示目前还没有释放池,或者有一个空占位符池,但是还没有加入对象
        assert(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
    
    
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            // 如果是空占位符池,需要加入一个释放池边界
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
    
    //如果没有创建一个autoreleasepool,则直接抛异常
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
    
     // 如果传入 POOL_BOUNDARY 则设置空池占位符
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

        // We are pushing an object or a non-placeholder'd pool.
        // 初始化一个 page 并设置 hotPage
        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // 插入释放池边界
        if (pushExtraBoundary) {
    
    
            page->add(POOL_BOUNDARY);
        }
        
        //  obj添加到page中
        return page->add(obj);
    }

autoreleaseNoPage只有在自动释放池还没有page时调用,主要逻辑:

  • 如果当前自动释放池推入的是一个哨兵POOL_BOUNDARY时,将EmptyPoolPlaceholder存入 TLS 中。
  • 如果 TLS 存储了EmptyPoolPlaceholder时,在创建好page之后,会先推入一个POOL_BOUNDARY,然后再将加入自动释放池的对象推入。
    推入都是调用了add函数:
// 向page中加入对象
    id *add(id obj)
    {
    
    
        assert(!full());
        unprotect();
        
        // 将next节点赋值给ret
        id *ret = next;  // faster than `return next-1` because of aliasing
        
        // 将obj赋值给next
        *next++ = obj;
        protect();
        return ret;
    }

这个很好理解,就是一个压入栈的操作。

objc_autoreleasePoolPop

在自动释放池所在作用域结束时,会调用objc_autoreleasePoolPop,对自动释放池中的对象进行释放。

// pop
    static inline void pop(void *token) 
    {
    
    
        AutoreleasePoolPage *page;
        id *stop;

        // 如果是空池占位符,要清空整个自动释放池
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
    
    
            // Popping the top-level placeholder pool.
            if (hotPage()) {
    
    
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                // 如果存在 hotPage ,则找到 coldPage 的起点 重新 pop
                pop(coldPage()->begin());
            } else {
    
    
                // 未使用过的释放池,置空 TLS 中存放的 hotPage.
                setHotPage(nil);
            }
            return;
        }

        // 根据token获取page
        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
    
    
            // 在 stop 不为 POOL_BOUNDARY 的情况下 只可能是 coldPage()->begin()
            if (stop == page->begin()  &&  !page->parent) {
    
    
                // 弹出顶层池,使冷页保持原位
                // 没有池的对象将被自动释放
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
    
    
                // 如果不是 POOL_BOUNDARY 也不是 coldPage()->begin() 则报错
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        /// 释放 stop 后面的所有对象
        page->releaseUntil(stop);

        // 清除空节点
        if (DebugPoolAllocation  &&  page->empty()) {
    
    
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
    
    
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        }
        // 清除后续节点 page
        else if (page->child) {
    
    
            // 如果当前 page 没有达到半满,则干掉所有后续 page
            if (page->lessThanHalfFull()) {
    
    
                page->child->kill();
            }
            // 如果当前 page 达到半满以上,则保留下一页
            else if (page->child->child) {
    
    
                page->child->child->kill();
            }
        }
    }

这段代码逻辑:

  • 检查入参是否为空池占位符EMPTY_POOL_PLACEHOLDER,如果是则继续判断是否hotPage存在,如果hotPage存在则将释放的终点改成coldPage()->begin(),如果hotPage不存在,则置空 TLS 存储中的hotPage
  • 检查stop既不是POOL_BOUNDARY也不是coldPage()->begin()的情况将报错。
  • 清空自动释放池中stop之后的所有对象。
  • 判断当前page如果没有达到半满,则干掉所有后续所有 page,如果超过半满则只保留下一个page。

这里用到了几个函数,首先是通过token获取page的pageForPointer:

 static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
    {
    
    
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;
// 将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:
        assert(offset >= sizeof(AutoreleasePoolPage));

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:

p = 0x100816048
p % SIZE = 0x48
result = 0x100816000
而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage。

通过检查 magic_t 结构体中的某个成员是否为 0xA1A1A1A1。

releaseUntil 释放对象

实现如下:

 // MARK: - 释放obj
    void releaseUntil(id *stop) 
    {
    
    
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        // 遍历链表
        while (this->next != stop) {
    
    
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            
            // 获取page的父指针
            while (page->empty()) {
    
    
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            
            // 根据next指针缩小一个位置获取obj
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
    
    
                
                // 发送release 消息
                objc_release(obj);
            }
        }

        setHotPage(this);

在这里通过一个while循环,判断当前page如果被清空,则继续清理链表中的上一个page,release了page里面存储的对象。

kill() 方法

 void kill() 
    {
    
    
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        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的调用栈

在这里插入图片描述
首先就是我们创建autoreleasePool,就是从autoreleasePoolPush开始到Pop结束。

- [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)

autorelease方法的调用栈中,最终都会调用上面提到的 autoreleaseFast 方法,将当前对象加到 AutoreleasePoolPage 中。

RunLoop和autoreleasePool

iOS在主线程的RunLoop中注册了两个Observer:

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();
  • 第2个Observer:
    ① 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
    ② 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。

猜你喜欢

转载自blog.csdn.net/chabuduoxs/article/details/126326184