iOS底层-内存管理之自动释放池

902 阅读7分钟

前言

内存管理里的篇章还有个自动释放池(AutoreleasePool),它是一种内存回收机制,放入autoreleasePool中的对象会延迟释放。那么这个自动释放池是什么,又有怎样的特性?下面我们将对它进行深入分析

结构分析

  • main.m@autoreleasepool,通过clang查看如下:

    截屏2021-11-08 09.46.53.png

    C++源码中@autoreleasepool是个__autoreleasepool的构造函数,通过搜索AtAutoreleasePool发现其代码如下:

    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };
    
    • 通过观察结构,发现在构造函数时会调用objc_autoreleasePoolPush,析构时调用objc_autoreleasePoolPop,再结合OC断点和汇编,最终确定代码在libobjc.A.dylib,于是在objc4-818.2源码中分析核心代码
  • 通过搜索objc_autoreleasePoolPushobjc_autoreleasePoolPop发现,他们的源码比较类似都是调用AutoreleasePoolPage中的函数,一个是调用push,一个是调用pop

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

    下面先分析下AutoreleasePoolPage

AutoreleasePoolPage

    1. 找到AutoreleasePoolPage,在方法的开头可以看到注释:

    截屏2021-11-08 11.35.29.png

    • 在注释中可以得到:自动释放池存放的是指针,这些指针要么指向对象,要么指向释放池的边界POOL_BOUNDARY,池中还有一个哨兵对象,自动释放池是一个双向链表
    1. AutoreleasePoolPage有自己的构造函数和析构函数: 截屏2021-11-08 11.36.14.png

AutoreleasePoolPageData

  • AutoreleasePoolPage继承AutoreleasePoolPageData,它是一个结构体

    截屏2021-11-09 23.13.36.png

    相关参数作用如下:

    • magic:用来校验AutoreleasePoolPage的结构是否完整
    • next:指向最新添加的autoreleased对象下一个位置,就是这个对象初始化时指向的begin()
    • thread:指向当前线程
    • parent:指向父结点,第一个节点的parent值为nil
    • child:指向子结点,最后一个结点的child值为nil
    • depth:代表深度,从0开始往后递增1。也就是每增一页就递增1
    • hiwat:代表high water mark,最大入栈数量标记
  • 下面在代码中分析AutoreloeasePool的结构

代码分析

  • 创建一个空的工程,将环境改成MRC然后在@autoreleasePool中加入对象,并打印autoreleasePool中的相关内容:

    extern void _objc_autoreleasePoolPrint(void);
    
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] autorelease];
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
    
      1. _objc_autoreleasePoolPrint是在objc源码中是一个打印自动释放池内容的函数。
      1. @autoreleasepool是自动释放池,autorelease是将对象加入自动释放池
  • 打印结果如下:

    截屏2021-11-11 23.39.47.png

    • 可以观察到自动释放池前面有56字节的一些相关初始化成员变量,然后有个8字节哨兵对象,之后才是加入自动释放池的对象。

首页结构图

  • 根据上面的分析,autoreleasePool第一页的结构如下图

    截屏2021-11-12 10.31.31.png

自动释放池是怎么加入销毁对象的,一页能够加入多少销毁对象呢,以及对象的释放在什么时候?下面我们将从原理去分析这些特性

原理

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

    不看debug的代码,于是可以定位到autoreleaseFast代码

  • autoreleaseFast代码如下:

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage(); // 获取hotpage
        if (page && !page->full()) { // 如果page存在且没有存满,则添加对象
            return page->add(obj); 
        } else if (page) { // 存满则新增
            return autoreleaseFullPage(obj, page); 
        } else { // page不存在,则创建page
            return autoreleaseNoPage(obj);
        }
    }
    
    • hotPage取到page后主要分为三种情况:
        1. page存在且没有满,则添加
        1. page满了,则调用autoreleaseFullPage方法分页后添加
        1. page不存在就调用autoreleaseNoPage进行相关创建并添加操作

    下面针对这三种情况进行详细分析

add

  • add代码的核心是个内存平移的操作

    截屏2021-11-12 15.23.21.png

    • next指针最开始指向begin位置,当进来一个objnext就会往下平移一个单位直到存满
    • next指针平移过程如下图:

    截屏2021-11-12 16.34.47.png

判断存满full

  • 首先来看看存满的条件full

    截屏2021-11-12 16.26.39.png

    • next指向end位置,也就是page的末尾时就代表存满了,可以看到size的最大值是2^12也就是4096,由于里面的成员变量占56字节,哨兵对象占8字节,所以可以添加的对象占4032字节,也就是可以添加504个销毁对象。
  • 代码验证:

    截屏2021-11-12 16.45.14.png

    • 当添加第505个时就会进行分页,打印如下

      截屏2021-11-12 16.48.41.png

      • 505个对象添加在了新的page中,新的page中处理初始化需要的成员变量外,并没有哨兵对象,也就是新增的页面占用4040个字节,也就是 可以存储505个对象
    • 疑问:为什么第二页没有哨兵对象?

      • 答:因为哨兵对象有"监控"page边界的作用,第一页已经满了已经不需要add操作,所以它可以去第二页工作,由于没有存满之前都在当前页工作,所以只需要一个哨兵对象就足够了

结论:
1. 第一页最多可以存储504个对象,新增页最多存储505个对象
2. 新增页没有哨兵对象

autoreleaseFullPage

  • 当存满后会走autoreleaseFullPage方法

    截屏2021-11-12 17.06.11.png

    • 此处主要是do-while循环中找到最后一张page
      • 如果没有满,则将page设置成hot,再调用add添加obj
      • 如果满了,就调用AutoreleasePoolPage创建新page,然后再add添加obj
  • AutoreleasePoolPage方法主要是进行page初始化,并设置成员变量的值,如果有parent,则设置parentchild为自己

    截屏2021-11-12 17.12.18.png

autoreleaseNoPage

  • 当没有取到hotPage,也就是第一个page不存在时就会进行创建:

    截屏2021-11-12 17.18.26.png

    • 创建的过程和上面存满的过程一样,但第一个界面创建后会先添加一个哨兵对象,然后再添加销毁对象

pop

  • pop主要代码如下

    截屏2021-11-12 19.21.46.png

    • 如果token指向哨兵对象,则说明第一页是空的,然后做一些相应处理
    • popPageDebug方法最终会走到popPage函数,再查看popPage的实现

popPage

代码如下

截屏2021-11-12 19.35.51.png

    1. popPage首先会调用releaseUntil将对象进行release

    截屏2021-11-12 19.49.17.png

    • 这块代码主要是在while循环中进行内存平移取到对象,然后调用objc_release方法进行释放
    • 平移取对象的过程如下图:

    截屏2021-11-12 19.56.33.png

    1. 对象释放后,然后当前页是空的,就会找到parent,然后将当前页kill,并将parent设置成hotPage
    1. 如果是page是第一页,则直接kill,并将hotPage设置为nil
    1. 如果当前pagechild
    • 如果当前页半满,则将child直接kill
    • 如果child还有child,则将childchild直接kill

kill

  • page销毁的核心是kill函数,它的实现如下:

    截屏2021-11-12 20.04.58.png
    kill,流程比较简单,就是获取当前页面的child,从最后一张开始page开始delete,直到当前页才停止

整个pop过程是:先将销毁对象release,然后从最后一张开始删除,直到第一张。第一张page销毁对象release后将hotPage置为nil

多page结构图

截屏2021-11-12 20.24.06.png

相关题目

    1. AutoreleasePool能够嵌套使用吗?
    • 使用代码测试如下
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSObject *obj1 = [[NSObject alloc] autorelease];
            NSLog(@"obj1 : %@", obj1);
          
            @autoreleasepool {
                NSObject *obj2 = [[NSObject alloc] autorelease];
                NSLog(@"obj2 : %@", obj2);
                _objc_autoreleasePoolPrint();
            }
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
    
    • 打印结果:

    截屏2021-11-12 23.23.51.png

    • 从结果中可以看出,第一次打印里面有两个哨兵对象,也就是包含的自动释放池被压进了外面的自动释放池,所以打印会有两个哨兵对象,而出作用域后,里面的自动释放池释放了,所以下面的打印只有一个哨兵对象和obj1了。

    • 答:自动释放池可以嵌套使用

    1. ARC中的对象怎么加入自动释放池的?
    • 我们知道在MRC中,创建的对象需要加上autorelease才可以入池,在ARC中没有autorelease,可以使用__autoreleasing修饰,将对象加入自动释放池:
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSObject *obj1 = [NSObject alloc] ;
            NSLog(@"obj1 : %@", obj1);
          
            NSObject * __autoreleasing obj2 = [NSObject alloc] ;
            NSLog(@"obj2 : %@", obj2);
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
    

    打印结果如下:

    截屏2021-11-13 00.01.28.png

    • ARC入池主要有以下情形:如果不是alloc/new/copy/mutableCopy创建,则自动将返回对象注册到Autoreleasepool