iOS AutoReleasePool的实现原理

iOS AutoReleasePool的实现原理

[TOC]

本文也属于iOS Objective-C 内存管理的范畴,AutoReleasePool就是自动释放池,下面我们来探索一下。

1. 什么是AutoReleasePool

AutoReleasePool是Objective—C中一种内存自动回收的机制,他可以将加入AutoReleasePool中的变量release的时机延迟。也就是说,当你创建一个对象,在正常情况下,变量会在超出其作用域的时候立即release,如果将该对象加入到自动释放池中,这个对象并不会立即释放,而是等到runloop休眠或者超出AutoReleasePool的作用域{}之后才会被释放。

自动释放池示意图:

image
  1. 这幅图演示了从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop
  2. 用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等
  3. runloop 在监听到交互时间后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
  4. 在 一次完整的runloop结束前,会想自动释放池中所有对象发送release消息,然后销毁自动释放池

2. AutoReleasePool 的实现

2.1 通过clang分析

分析AutoReleasePool我们首先从clang入手:

编写如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

使用如下命令,通过clang编译为底层代码:

xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m

编译后的代码为:

struct __AtAutoreleasePool {
    // 构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    // 析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        // 结构体
        __AtAutoreleasePool __autoreleasepool;
    }
    return 0;
}

通过clang编译后的代码我们可以发现,AutoReleasePool的本质是一个结构体。

  • 在底层是__AtAutoreleasePool
  • 有构造函数和析构函数
  • 结构体在其作用域结束的时候会自动调用析构函数
  • 这里的作用域指的就是{}
  • 构造函数中会调用objc_autoreleasePoolPush
  • 析构函数中会调用objc_autoreleasePoolPop

析构函数

关于C++中的析构函数,下面我们举个例子来说明一下:

struct Test{
    Test
(){
        printf("1111 - %s\n", __func__);
    }
    ~Test(){
        printf("2222 - %s\n", __func__);
    }
};

int main(int argc, const char * argv[]) {
    
    {
        Test test;
    }
    return 0;
}

<!--打印结果-->
1111 - Test
2222 - ~Test

根据打印结果我们可以轻松的得出如下结论:

  1. 对于Test结构体创建对象的时候会自动调用构造函数
  2. 在出了作用域{}后会自动调用析构函数

2.2 通过汇编分析

还是这段代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

main那行添加断点,开始汇编调试Debug->Debug Workflow->Always Show Disassembly,运行,结果如下:

image

我们可以看到这里面调用了两个符号objc_autoreleasePoolPushobjc_autoreleasePoolPop,这跟我们在clang在的结果是一样的。

2.3 小结

通过初步的探索,我们初步了解了AutoReleasePool下面稍作总结:

  1. AutoReleasePool本质是一个结构体对象
  2. 加入自动释放池时调用objc_autoreleasePoolPush方法
  3. 在调用析构函数的时候会释放这些对象,通过objc_autoreleasePoolPop方法

3. 底层分析

这里我使用的是objc4-818.2这个版本的源码:

3.1 初步探索

首先我们搜索了一下objc_autoreleasePoolPush,这里就可以看到objc_autoreleasePoolPushobjc_autoreleasePoolPop的实现。

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

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


void *
_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void
_objc_autoreleasePoolPop(void *ctxt)
{
    objc_autoreleasePoolPop(ctxt);
}

通过这些找到了AutoreleasePoolPage,在NSObject.mm文件中,这里我们大概就能够知道自动释放池是一个页,看看注释:

/***********************************************************************

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.
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.
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.
**********************************************************************/

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,也就是哨兵,现在也叫边界。
  3. 自动释放池是一个页的结构,而这个页是一个双向链表
  4. 自动释放池与线程是息息相关的

了解了这些后我们就要探索一下如下问题:

  1. 自动释放池是什么时候创建的呢?
  2. 对象是如何加入到自动释放池的呢?
  3. 哪些对象才会加入到自动释放池呢?
  4. 这个池子的大小是怎样的?

3.2 AutoreleasePoolPage

这里我们研究一下AutoreleasePoolPage,从上面的代码就可以看出:

  1. pushpop都是AutoreleasePoolPage中的方法
  2. AutoreleasePoolPage是一个类
  3. 也是一个页,,这个页的大小是4096字节

关于4096的定义如下:

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

public:
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif

……

#define PAGE_MAX_SHIFT          14
#define PAGE_MAX_SIZE           (1 << PAGE_MAX_SHIFT)
#define PAGE_MAX_MASK           (PAGE_MAX_SIZE-1)

#define PAGE_MIN_SHIFT          12
#define PAGE_MIN_SIZE           (1 << PAGE_MIN_SHIFT)
#define PAGE_MIN_MASK           (PAGE_MIN_SIZE-1)
}

这里的1左移12位就是4096。

剩下的代码很多,这里就不都不都放了,感兴趣的自己去下载一下objc4_debug

这里我们看到AutoreleasePoolPage继承自AutoreleasePoolPageData,并且它的属性也是来自父类,下面我们看看AutoreleasePoolPageData

3.3 AutoreleasePoolPageData

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;

        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
    static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif

    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    // 初始化函数AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

从源码中我们可以看到很多属性,下面我们拆解开来:

//用来校验AutoreleasePoolPage的结构是否完整
magic_t const magic;//16个字节
//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
__unsafe_unretained id *next;//8字节
//指向当前线程
pthread_t const thread;//8字节
//指向父节点,第一个结点的parent值为nil
AutoreleasePoolPage * const parent;//8字节
//指向子节点,最后一个结点的child值为nil
AutoreleasePoolPage *child;//8字节
//表示深度,从0开始,往后递增1
uint32_t const depth;//4字节
//表示high water mark 最大入栈数量标记
uint32_t hiwat;//4字节

其中AutoreleasePoolPageData结构体的内存大小为56字节:

  • 其中属性magic的类型是magic_t结构体,所占内存大小为m[4];也就是4*4 = 16字节
  • next指针占8字节
  • threadparentchild也都占8字节
  • depthhiwat类型为uint32_t,也就是unsigned int类型,均占4字节

所以这些加起来就是16+8+8+8+8+4+4 = 56。

3.4 push

下面我们研究一下push,直接看源码:

static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // 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;
    }

代码不多,其逻辑分析如下:

  1. 首先判断是否有pool,这里也就是有没有页
  2. 如果没有则通过autoreleaseNewPage方法创建一页
  3. 如果有,则调用autoreleaseFast方法

3.5 autoreleaseNewPage

3.5.1 autoreleaseNewPage

下面我们在看看autoreleaseNewPage,该方法就是创建一个新的页。源码如下:

// 创建新页
static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {
        // 获取当前操作页
        AutoreleasePoolPage *page = hotPage();
        if (page) return autoreleaseFullPage(obj, page);
        else return autoreleaseNoPage(obj);
    }

根据上面的代码我们可以知道:

  1. 首先获取当前的操作页,也就是热页
  2. 如果存在则调用autoreleaseFullPage方法
  3. 如果不存在则调用autoreleaseNoPage方法

3.5.2 hotPage

获取hotpage

static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
  1. 通过tls_get_direct方法获取当前线程的的页
  2. 如果是一个空的就返回nil
  3. 不是则进一步处理

3.5.3 autoreleaseNoPage

这一节我们先来看看autoreleaseNoPage方法,其他的放在下面在分析。

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
        ASSERT(!hotPage());
        // 定义一个bool值变量
        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;
        }
        // 如果压栈的不是哨兵对象,并且没有pool 则报错
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // 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", 
                         objc_thread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        // 如果是哨兵对象,并且没有申请自动释放池的内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // 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.

        // Install the first page.
        // 初始化一页
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        // 并将该页设置为聚焦页
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        // pushExtraBoundary为true则压栈哨兵对象
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        // 添加对象到自动释放池的这一页
        return page->add(obj);
    }

根据上面的代码我们可以看到,当前线程的自动释放池页是由AutoreleasePoolPage的构造方法直接创建的,代码如下:

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),// 当前线程
                                newParent,
                                newParent ? 1+newParent->depth : 0,如果是第一页深度为0,然后不断+1
                                newParent ? newParent->hiwat : 0)
    {
        if (objc::PageCountWarning != -1) {
            checkTooMuchAutorelease();
        }

        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            // this 表示新建页面,将当前页面的子节点 赋值为新建页面
            parent->child = this;
            parent->protect();
        }
        protect();
    }

代码中AutoreleasePoolPageData方法传入的参数含义为:

  • begin()表示压栈的位置,也就是下一个能压栈的位置
id * begin() {
        //等于 首地址+56(AutoreleasePoolPage类所占内存大小)
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
  • objc_thread_self()表示的是当前线程,而当前线程是通过tls获取的
__attribute__((const))
static inline pthread_t objc_thread_self()
{
    //通过tls获取当前线程
    return (pthread_t)tls_get_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
}
  • newParent表示父节点
  • 后面这俩就是通过父节点计算深度和最大入栈个数,也就是计算得出depthhiwat

3.5.4 通过lldb查看内存结构

由于在ARC模式下无法直接调用autorelease,所以我们将demo切换至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting)设置为NO。这里我是在源码工程里面进行操作的,objc源码的下载上面有提到。编写如下代码:

// 打印自动释放池结构的方法这里未来路绿绿调用直接extern一下
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //循环创建对象,并加入自动释放池
        for (int i = 0; i < 5; i++) {
             NSObject *objc = [[NSObject alloc] autorelease];
        }
        //打印
        _objc_autoreleasePoolPrint();
    }
}

运行查看结果:

image

我们可以看到有6个,其中第一个是哨兵对象,后面的5个是我们压栈的NSObject对象。

但是前面还有些内容,这些占用的内存大小是0x38也就是56字节,与AutoreleasePoolPage中属性占用内存的大小一致。

下面我们就不禁有个疑问,这一页能存储多少个对象呢?前面有提到这一页的size是4096,所以剩下的能存储505个8字节的对象,所以我们将上面示例代码的5修改为505,运行查看结果:

image
image

太多了,截图了两个图,我们可以看到,此时就有两页了,第一页的状态是PAGE (full) (cold),第二页的状态是PAGE (hot),第一页存储了除哨兵外的504个需要释放的对象,第二有人存储了1个要释放的对象。第二页的一开始并没有存储哨兵对象。下面我们增加数量为505+506,运行查看结果如下:

image

image
image

可以发现第一页还是存储504个需要释放的对象,第二页505个,第三页2个。

基本我们可以得出如下结论:

  • 第一页拥有一个哨兵对象,加上504个需要释放的对象,当第一页满了,就会开辟新的一页
  • 从第二页开始不再有哨兵对象了,而是全部用来存储需要释放的对象,最多505
  • 所以一页的大小等于505*8 = 4040字节,加上56刚好为4096

盗个图:


image

其实还有个问题,如果是多线程呢?这个页面中是怎么存储的呢?

我们编写如下代码

// 打印自动释放池结构的方法这里未来路绿绿调用直接extern一下
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"thread 1 %@", [NSThread currentThread]);
        NSObject *objc = [[NSObject alloc] autorelease];
        NSLog(@"first objc = %@",objc);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"thread 2 %@", [NSThread currentThread]);
            @autoreleasepool {
                NSObject *objc1 = [[NSObject alloc] autorelease];
                NSLog(@"two objc = %@",objc1);
                _objc_autoreleasePoolPrint();
            }
           
        });
        _objc_autoreleasePoolPrint();
    }
    
    sleep(2);
    return 0;
}

打印结果:

image

我们可以看到在多线程和@autoreleasepool嵌套的时候会有2个哨兵的情况出现,所以一页存储需要释放的对象个数就又减少了一个。

如果我们在嵌套一层呢?修改代码为如下:

// 打印自动释放池结构的方法这里未来路绿绿调用直接extern一下
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"thread 1 %@", [NSThread currentThread]);
        NSObject *objc = [[NSObject alloc] autorelease];
        NSLog(@"first objc = %@",objc);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"thread 2 %@", [NSThread currentThread]);
            @autoreleasepool {
                NSObject *objc1 = [[NSObject alloc] autorelease];
                NSLog(@"two objc = %@",objc1);
                _objc_autoreleasePoolPrint();
                
                dispatch_async(dispatch_get_global_queue(0, 0), ^{
                    NSLog(@"thread 3 %@", [NSThread currentThread]);
                    @autoreleasepool {
                        NSObject *objc2 = [[NSObject alloc] autorelease];
                        NSLog(@"three objc = %@",objc2);
                        _objc_autoreleasePoolPrint();
                    }

                });
            }
           
        });
        _objc_autoreleasePoolPrint();
    }
    
    sleep(5);
    return 0;
}

打印结果:

image

根据打印结果我们可以看到最多还是两个哨兵。

3.6 autoreleaseFast

研究完了新建一页后我们再来研究一下压栈对象时的这个方法,源码如下:

static inline id *autoreleaseFast(id obj)
{
    // 获取聚焦页
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 没满就add
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

代码也很简单:

  1. 首先获取到hotpage
  2. 判断page存在且没满的话将压栈
  3. 满了后page存在则调用autoreleaseFullPage,满处理方法
  4. 最后就是没有page则调用autoreleaseNoPage方法,这个在上面讲过了

3.6.1 autoreleaseFullPage

autoreleaseFullPage方法主要是在页满的时候进行处理的,源码如下:

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

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}
  1. 这里面通过一个do while循环查找子页面,并判断子页面是否也是满的
  2. 如果满就继续循环,如果没有子页面,就创建个新页面

3.6.2 add

其实真正的压栈方法是add,下面我们就看看这个add是怎么实现的,源码如下:

id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret;

#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        if (!DisableAutoreleaseCoalescing || !DisableAutoreleaseCoalescingLRU) {
            if (!DisableAutoreleaseCoalescingLRU) {
                if (!empty() && (obj != POOL_BOUNDARY)) {
                    AutoreleasePoolEntry *topEntry = (AutoreleasePoolEntry *)next - 1;
                    for (uintptr_t offset = 0; offset < 4; offset++) {
                        AutoreleasePoolEntry *offsetEntry = topEntry - offset;
                        if (offsetEntry <= (AutoreleasePoolEntry*)begin() || *(id *)offsetEntry == POOL_BOUNDARY) {
                            break;
                        }
                        if (offsetEntry->ptr == (uintptr_t)obj && offsetEntry->count < AutoreleasePoolEntry::maxCount) {
                            if (offset > 0) {
                                AutoreleasePoolEntry found = *offsetEntry;
                                memmove(offsetEntry, offsetEntry + 1, offset * sizeof(*offsetEntry));
                                *topEntry = found;
                            }
                            topEntry->count++;
                            ret = (id *)topEntry;  // need to reset ret
                            goto done;
                        }
                    }
                }
            } else {
                if (!empty() && (obj != POOL_BOUNDARY)) {
                    AutoreleasePoolEntry *prevEntry = (AutoreleasePoolEntry *)next - 1;
                    if (prevEntry->ptr == (uintptr_t)obj && prevEntry->count < AutoreleasePoolEntry::maxCount) {
                        prevEntry->count++;
                        ret = (id *)prevEntry;  // need to reset ret
                        goto done;
                    }
                }
            }
        }
#endif
        ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        // Make sure obj fits in the bits available for it
        ASSERT(((AutoreleasePoolEntry *)ret)->ptr == (uintptr_t)obj);
#endif
     done:
        protect();
        return ret;
    }

看着好多代码,其实很简单,主要就是通过next指针的偏移,来进行不断的添加。

3.7 autorelease

刚刚我们测试每页能存储多少对象,以及查看内存结构的时候使用到了autorelease,下面我们看看它的底层是如何实现的。

直接查看其源码:

// Equivalent to [this autorelease], with shortcuts if there is no override
inline id 
objc_object::autorelease()
{
    ASSERT(!isTaggedPointer());
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootAutorelease();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}

// Base autorelease implementation, ignoring overrides.
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

public:
    static inline id autorelease(id obj)
    {
        ASSERT(!obj->isTaggedPointerOrNil());
        id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
        ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
#endif
        return obj;
    }

可以看到,最后还是调用的autoreleaseFast方法。

3.8 pop

下面我们再来看看pop

objc_autoreleasePoolPop方法中有个参数,在clang分析时,发现传入的参数是push压栈后返回的哨兵对象,也就是ctxt,其目的是避免出栈混乱,防止将别的对象出栈。

pop源码如下:

__attribute__((noinline, cold))
static void
popPageDebug(void *token, AutoreleasePoolPage *page, id *stop)
{
    popPage<true>(token, page, stop);
}

static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    // 判断对象是否是空占位符
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        // 获取聚焦页
        page = hotPage();
        if (!page) {
            // Pool was never used. Clear the placeholder.
            // 如果页不存在,则将聚焦页设置为nil
            return setHotPage(nil);
        }
        // Pool was used. Pop its contents normally.
        // Pool pages remain allocated for re-use as usual.
        // 如果存在则将该页设置为clodPage,token设置为开始的位置
        page = coldPage();
        token = page->begin();
    } else {
        // 获取token所在的页
        page = pageForPointer(token);
    }

    stop = (id *)token;
    // 判断stop 的值是否是哨兵,也就是最后的是不是哨兵
    if (*stop != POOL_BOUNDARY) {
        // 如果最后一个位置不是哨兵,最后也是第一个位置,并且没有父节点什么也不做
        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 {
            // Error. For bincompat purposes this is not
            // fatal in executables built with old SDKs.
            // 到这里就是出现了混乱,调用badPop方法
            return badPop(token);
        }
    }

    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }

    // 出栈该页
    return popPage<false>(token, page, stop);
}

根据pop源码,其主要是找到要pop的这页,进行一些容错处理,通过popPage出栈页。

3.8.1 popPage

下面我们看看popPage方法:

template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    //allowDebug传入的是false
    if (allowDebug && PrintPoolHiwat) printHiwat();
    // 释放当前操作页面的对象
    page->releaseUntil(stop);

    // memory: delete empty children 删除空的子页
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        // allowDebug为false也到不了这个分支
        // 获取到父页,将该页kill,设置父页为hot
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top)
        // when debugging missing autorelease pools
        //特殊情况:调试丢失的自动释放池时删除pop(top)的所有内容
        page->kill();
        setHotPage(nil);
    } else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

3.8.2 releaseUntil

下面我们看看releaseUntil,源码如下:

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
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

            // create an obj with the zeroed out top byte and release that
            id obj = (id)entry->ptr;
            int count = (int)entry->count;  // grab these before memset
#else
            id obj = *--page->next;
#endif
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                // release count+1 times since it is count of the additional
                // autoreleases beyond the first one
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);
                }
#else
                objc_release(obj);
#endif
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }

这里面的源码也很简单,就是一个while循环,通过next指针循环获取上一个对象,对于哨兵对象最后通过objc_release进行释放。

3.8.3 kill

下面我们看看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);
}

这里也很简单,首先找到最后一个子节点,然后循环置空子节点,从子向父遍历。

4. 总结

至此我们对自动释放池的分析就基本完毕了,下面总结一下:

首先总结一下push

  • 当没有pool时,即只有空占位符(存在tls中)时,则创建页,压栈哨兵对象
  • 在页中压栈普通对象主要通过next指针递增进行的
  • 当页满了,则新建一个子页,当前页的child指向子页,继续向子页添加要释放的对象

盗个图:


image
  • 出栈的时候则在页中通过next指针递减进行查找对象最后通过objc_release释放对象
  • 当该页为空的时候则将page赋值为父页

盗个图:


image

5.参考文档

iOS-底层原理 33:内存管理(三)AutoReleasePool & NSRunLoop 底层分析

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

推荐阅读更多精彩内容