autoreleasepool原理分析

作者: 天空像天空一样蓝 | 来源:发表于2021-09-17 18:07 被阅读0次
    @autoreleasepool {
        // Code benefitting from a local autorelease pool.
    }
    

    1、原理分析

    1.1、__AtAutoreleasePool

    下面我们先通过macOS工程来分析@autoreleasepool的底层原理。 macOS工程中的main()函数什么都没做,只是放了一个@autoreleasepool

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

    通过 Clang clang -rewrite-objc main.m 将以上代码转换为 C++ 代码。

    struct __AtAutoreleasePool {
        __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
            atautoreleasepoolobj = objc_autoreleasePoolPush();
        }
        ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
        void * atautoreleasepoolobj;
    };
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ 
        { __AtAutoreleasePool __autoreleasepool;  }
        return 0;
    }
    

    可以看到:

    • @autoreleasepool底层是创建了一个__AtAutoreleasePool结构体对象;

    • 在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj(POOL_BOUNDARY存放的内存地址,下面会讲到);

    • 在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。

    1.2、AutoreleasePoolPage

    下面我们进入Runtime objc4源码查看以上提到的两个函数的实现。

    // NSObject.mm
    void * objc_autoreleasePoolPush(void)
    {
        return AutoreleasePoolPage::push();
    }
    
    void objc_autoreleasePoolPop(void *ctxt)
    {
        AutoreleasePoolPage::pop(ctxt);
    }
    

    可以得知,
    objc_autoreleasePoolPush()objc_autoreleasePoolPop()两个函数其实是调用了AutoreleasePoolPage类的两个类方法push()pop()。所以@autoreleasepool底层就是使用AutoreleasePoolPage类来实现的。

    自动释放池的数据结构

    • 自动释放池的主要数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
    • 调用了 autorelease的对象最终都是通过 AutoreleasePoolPage对象来管理的;

    下面我们来看一下AutoreleasePoolPage类的定义:

    class AutoreleasePoolPage 
    {
    #   define EMPTY_POOL_PLACEHOLDER ((id*)1)  // EMPTY_POOL_PLACEHOLDER:表示一个空自动释放池的占位符
    #   define POOL_BOUNDARY nil                // POOL_BOUNDARY:哨兵对象
        static pthread_key_t const key = AUTORELEASE_POOL_KEY;
        static uint8_t const SCRIBBLE = 0xA3;   // 用来标记已释放的对象
        static size_t const SIZE =              // 每个 Page 对象占用 4096 个字节内存
    #if PROTECT_AUTORELEASEPOOL                 // PAGE_MAX_SIZE = 4096
            PAGE_MAX_SIZE;  // must be muliple of vm page size
    #else
            PAGE_MAX_SIZE;  // size and alignment, power of 2
    #endif
        static size_t const COUNT = SIZE / sizeof(id);  // Page 的个数
    
        magic_t const magic;                // 用来校验 Page 的结构是否完整
        id *next;                           // 指向下一个可存放 autorelease 对象地址的位置,初始化指向 begin()
        pthread_t const thread;             // 指向当前线程
        AutoreleasePoolPage * const parent; // 指向父结点,首结点的 parent 为 nil
        AutoreleasePoolPage *child;         // 指向子结点,尾结点的 child  为 nil
        uint32_t const depth;               // Page 的深度,从 0 开始递增
        uint32_t hiwat;
        ......
    }
    
    

    整个程序运行过程中,可能会有多个AutoreleasePoolPage对象。从它的定义可以得知:

    • 自动释放池(即所有的AutoreleasePoolPage对象)是以栈为结点通过双向链表的形式组合而成;

    • 自动释放池与线程一一对应;

    • 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址

    其内存分布图如下:


    图片.png

    1.2.1、POOL_BOUNDARY

    在分析这些方法之前,先介绍一下POOL_BOUNDARY

    • POOL_BOUNDARY的前世叫做POOL_SENTINEL,称为哨兵对象或者边界对象;
    • POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题;
    • 每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;
    • 当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY
    • 当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY

    1.2.2、push

        static inline void *push() 
        {
            id *dest;
            if (DebugPoolAllocation) { // 出错时进入调试状态
                // Each autorelease pool starts on a new pool page.
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {
                dest = autoreleaseFast(POOL_BOUNDARY);  // 传入 POOL_BOUNDARY 哨兵对象
            }
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    

    当创建一个自动释放池时,会调用push()方法。push()方法中调用了autoreleaseFast()方法并传入了POOL_BOUNDARY哨兵对象。
    下面我们来看一下autoreleaseFast()方法的实现:

        static inline id *autoreleaseFast(id obj)
        {
            AutoreleasePoolPage *page = hotPage();     // 双向链表中的最后一个 Page
            if (page && !page->full()) {        // 如果当前 Page 存在且未满
                return page->add(obj);                 // 将 autorelease 对象入栈,即添加到当前 Page 中;
            } else if (page) {                  // 如果当前 Page 存在但已满
                return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去
            } else {                            // 如果当前 Page 不存在,即还没创建过 Page
                return autoreleaseNoPage(obj);         // 创建第一个 Page,并将 autorelease 对象添加进去
            }
        }
    

    autoreleaseFast()中先是调用了hotPage()方法获得未满的Page,从AutoreleasePoolPage类的定义可知,每个Page的内存大小为 4096个字节,每当Page满了的时候,就会创建一个新的PagehotPage()方法就是用来获得这个新创建的未满的Page
    autoreleaseFast()在执行过程中有三种情况:

    • ① 当前Page存在且未满时,通过page->add(obj)将autorelease对象入栈,即添加到当前Page中;
      ② 当前Page存在但已满时,通过autoreleaseFullPage(obj, page)创建一个新的Page,并将autorelease对象添加进去;
      ③ 当前Page不存在,即还没创建过Page,通过autoreleaseNoPage(obj)创建第一个Page,并将autorelease对象添加进去。

    1.2.3、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.
                    pop(coldPage()->begin());
                } else {
                    // Pool was never used. Clear the placeholder.
                    setHotPage(nil);
                }
                return;
            }
    
            page = pageForPointer(token);
            stop = (id *)token;
            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.
                    return badPop(token);
                }
            }
    
            if (PrintPoolHiwat) printHiwat();
    
            page->releaseUntil(stop);
    
            // memory: delete empty children
            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);
            } 
            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();
                }
            }
        }
    

    pop()方法的传参token即为POOL_BOUNDARY对应在Page中的地址。当销毁自动释放池时,会调用pop()方法将自动释放池中的autorelease对象全部释放(实际上是从自动释放池的中的最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY)。pop()方法的执行过程如下:

    • ① 判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池;
    • ② 如果不是的话,就通过pageForPointer(token)拿到token所在的Page(自动释放池的首个Page);
    • ③ 通过page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址;
    • ④ 判断当前Page是否有子Page,有的话就销毁。

    1.2.4、begin、end、empty、full

    下面再来看一下beginendemptyfull这些方法的实现。

    • begin的地址为:Page自己的地址+Page对象的大小56个字节;
    • end的地址为:Page自己的地址+4096个字节;
    • empty判断Page是否为空的条件是next地址是不是等于begin;
    • full判断Page是否已满的条件是next地址是不是等于end(栈顶)。
        id * begin() {
            return (id *) ((uint8_t *)this+sizeof(*this));
        }
    
        id * end() {
            return (id *) ((uint8_t *)this+SIZE);
        }
    
        bool empty() {
            return next == begin();
        }
    
        bool full() { 
            return next == end();
        }
    

    2、查看自动释放池的情况

    可以通过以下私有函数来查看自动释放池的情况:

    extern void _objc_autoreleasePoolPrint(void);
    

    3、iOS 工程示例分析

    在iOS工程中,方法里的autorelease对象是什么时候释放的呢?

    有系统干预释放和手动干预释放两种情况。

    • 系统干预释放是不指定@autoreleasepool,所有autorelease对象都由主线程的RunLoop创建的@autoreleasepool来管理。
    • 手动干预释放就是将autorelease对象添加进我们手动创建的@autoreleasepool中。

    下面还是在MRC环境下进行分析。

    3.1、系统干预释放

    我们先来看以下 Xcode 11 版本的iOS程序中的main()函数,和旧版本的差异。

    // Xcode 11
    int main(int argc, char * argv[]) {
        NSString * appDelegateClassName;
        @autoreleasepool {
            // Setup code that might create autoreleased objects goes here.
            appDelegateClassName = NSStringFromClass([AppDelegate class]);
        }
        return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }
    
    // Xcode 旧版本
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    新版本 Xcode 11 中的 main 函数发生了哪些变化?
    旧版本是将整个应用程序运行放在@autoreleasepool内,由于RunLoop的存在,要return即程序结束后@autoreleasepool作用域才会结束,这意味着程序结束后main函数中的@autoreleasepool中的autorelease对象才会释放。
    而在 Xcode 11中,触发主线程RunLoop的UIApplicationMain函数放在了@autoreleasepool外面,这可以保证@autoreleasepool中的autorelease对象在程序启动后立即释放。正如新版本的@autoreleasepool中的注释所写 “Setup code that might create autoreleased objects goes here.”(如上代码),可以将autorelease对象放在此处。

    接着我们来看 “系统干预释放” 情况的示例:

    - (void)viewDidLoad {
        [super viewDidLoad];    
        Person *person = [[[Person alloc] init] autorelease];    
        NSLog(@"%s", __func__);
    }
    
    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];    
        NSLog(@"%s", __func__);
    }
    
    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];    
        NSLog(@"%s", __func__);
    }
    
    // -[ViewController viewDidLoad]
    // -[ViewController viewWillAppear:]
    // -[Person dealloc]
    // -[ViewController viewDidAppear:]
    

    可以看到,调用了autorelease方法的person对象不是在viewDidLoad方法结束后释放,而是在viewWillAppear方法结束后释放,说明在viewWillAppear方法结束的时候,调用了pop()方法释放了person对象
    其实这是由RunLoop控制的,下面来讲解一下RunLoop@autoreleasepool的关系。

    3.2、RunLoop 与 @autoreleasepool

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

    第1个Observer

    • 监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()

    第2个Observer

    • ① 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    • ② 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
    图片.png

    所以,在iOS工程中系统干预释放的autorelease对象的释放时机是由RunLoop控制的,会在当前RunLoop每次循环结束时释放。以上person对象在viewWillAppear方法结束后释放,说明viewDidLoadviewWillAppear方法在同一次循环里。

    • kCFRunLoopEntry:在即将进入RunLoop时,会自动创建一个__AtAutoreleasePool结构体对象,并调用objc_autoreleasePoolPush()函数。
      -kCFRunLoopBeforeWaiting:在RunLoop即将休眠时,会自动销毁一个__AtAutoreleasePool对象,调用objc_autoreleasePoolPop()。然后创建一个新的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPush()
    • kCFRunLoopBeforeExit,在即将退出RunLoop时,会自动销毁最后一个创建的__AtAutoreleasePool对象,并调用objc_autoreleasePoolPop()

    3.3、手动干预释放

    我们再来看一下手动干预释放的情况。

    - (void)viewDidLoad {
        [super viewDidLoad];    
        @autoreleasepool {
            Person *person = [[[Person alloc] init] autorelease];  
        }  
        NSLog(@"%s", __func__);
    }
    
    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];    
        NSLog(@"%s", __func__);
    }
    
    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];    
        NSLog(@"%s", __func__);
    }
    
    // -[Person dealloc]
    // -[ViewController viewDidLoad]
    // -[ViewController viewWillAppear:]
    // -[ViewController viewDidAppear:]
    

    可以看到,添加进手动指定的@autoreleasepool中的autorelease对象,在@autoreleasepool大括号结束时就会释放,不受RunLoop控制。

    相关文章

      网友评论

        本文标题:autoreleasepool原理分析

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