美文网首页IOS知识积累
iOS AutoReleasePool的实现原理

iOS AutoReleasePool的实现原理

作者: just东东 | 来源:发表于2021-04-15 17:21 被阅读0次

    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 底层分析

    相关文章

      网友评论

        本文标题:iOS AutoReleasePool的实现原理

        本文链接:https://www.haomeiwen.com/subject/vifjlltx.html