自动释放池底层探索

215 阅读6分钟

前言

自动释放池这个东西我们新建工程在main文件里面就有@autoreleasepool的相关代码,那么它到底是啥,它有啥用途?今天我们就对它进行一番探索。

autoreleasepool源码引入

还原cpp代码

我们xcrunclang一下mian文件得到相关cpp代码:

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

搜索__AtAutoreleasePool这个结构体得到代码:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

这个意思是在进入作用域会调用__AtAutoreleasePool结构体的构造函数,进而调用objc_autoreleasePoolPush函数,出了作用域会调用结构体的析构函数,进而执行objc_autoreleasePoolPop函数。 在探索objc_autoreleasePoolPushobjc_autoreleasePoolPop之前我们关注下__AtAutoreleasePool的来源是怎么从@autoreleasepool变过来的

llvm查看__AtAutoreleasePool结构体

全局搜索llvm的代码找到__AtAutoreleasePoolx相关代码如下: image.png 以上是结构体定义的地方 image.png 以上解释了@autoreleasepool被替换成了/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;的原因

汇编探索@autoreleasepool

打开汇编,对@autoreleasepool打上断点跟进流程:

image.png

image.png

image.png

于是我们定位到objc_autoreleasePoolPush来自libobjc.A.dylib

autoreleasePoolPush源码分析

打开libobjc源码搜索objc_autoreleasePoolPush

image.png

AutoreleasePoolPage这个是啥,点击去发现是个,继承自AutoreleasePoolPageData

image.png

点进自AutoreleasePoolPageData去: image.png

查看AutoreleasePoolPageData的注释部分 image.png 这段话有几个关键点:

  • 线程相关,是个结构(先进后出),存的是8字节的指针
  • 存的指针是一个对象或者是一个boundary的哨兵边界
  • token指向边界用来释放池子
  • 双向链表结构
  • TSL存储hot

查看AutoreleasePoolPageData其中的成员变量作用如下:

  • magic ⽤来校验 AutoreleasePoolPage 的结构是否完整;
  • next 指向最新添加的 autoreleased 对象的下⼀个位置,初始化时指向 begin()
  • thread 指向当前线程;
  • parent 指向⽗结点,第⼀个结点的 parent 值为 nil
  • child 指向⼦结点,最后⼀个结点的 child 值为 nil
  • depth 代表深度,从 0 开始,往后递增 1
  • hiwat 代表 high water mark 最⼤⼊栈数量标记,这个一般用不到

AutoreleasePoolPage结构体大小

image.png 分析magic_t结构体大小: M0M1_len是全局区,不占堆区空间,m[4]占用4个4字节空间,所以magic_t结构体大小是16个字节 AutoreleasePoolPage的其余成员的大小: nextthreadparentchild都是指针占用8字节,depth占用4字节,hiwat占用4字节 所以AutoreleasePoolPage的结构体大小是16+8*4+4+4=56字节

objc_autoreleasePoolPush流程

image.png

小概率开启DebugPoolAllocation宏的话每次都会新开一页,我们就不进行探索这个流程了 大概率会走autoreleaseFast流程,点进去:

image.png 其中hotPage热页是存在TSL的:

image.png

image.png

autoreleaseFast流程:

  • TSL中获取热页
  • 热页存在并且没满的话走add(obj)流程
  • 热页存在并且满的话走autoreleaseFullPage流程
  • 热页不存在走autoreleaseNoPage流程

第一次进来热页不存在,我会进入autoreleaseNoPage流程: image.png

autoreleaseFullPage流程: image.png

add(obj)流程看就是正常的压栈流程: image.png

objc_autoreleasePoolPop流程

pop的流程就是push的逆流程,主要是出栈,更新热页,kill当前页,kill相关child

image.png

  • 删除页,需要先将页中存储的对象删除掉
  • 如果该页为空,直接删除,并且设置上一页为当前页
  • 如果该页已经是第一页了,就说明池子已空,就清空池子
  • 如果仍然有下一页,就先把下一页删掉
  • lessThanHalfFull的判断应该是为了删除后下次新加入对象可以节省分配页的操作

image.png

  • 这里就是删掉每一页,栈顶开始删
  • 删除的方式就是设置为nil

autoreleasePoolPop的边界探索

autoreleasePoolPop会有一个满页的情况,那么要压入多少个对象会满出来呢,带着这个问题我们进行探索。 首先导出一个打印autoreleasePoolPop结构的函数方便调试:

extern _objc_autoreleasePoolPrint(void);

image.png

image.png

我们尝试压入一个对象:

image.png

打印结果发现压入了只有一个对象,并且并不是我们想要的对象,这是什么原因呢? 原来我们现在是ARC环境,ARC加入到自动释放池有一定条件,我们先把工程改成MRC环境试试

MRC

image.png

设置里面自动引用计数开关设置为关

image.png MRC需要手动发送autorelease消息,这时候我们看到我们的对象加入到了自动释放池,另外一个自然是哨兵对象了。

我们多加入一些对象探索一个池能加多少对象:

@autoreleasepool {
  for (int i = 0; i < 504; i ++) {
    SPObject *sp = [[SPObject alloc] autorelease];
  }
  _objc_autoreleasePoolPrint();
}

image.png

我们加入504个对象后出现了full标记,此时page数目还是1个。 加入504改成505结果如下:

image.png

image.png

这时候就分配了新的page,并且新的page是热页同时存储了一个对象(这一个没有哨兵对象),同时第一页的热页标记去掉了。

既然第二页没有存哨兵对象,那么我们第二页应该可以加入505个对象: 第一页: image.png

第二页: image.png

第三页: image.png

运行结构跟我们猜想的一样。看到这里读者是否好奇为什么是504,505这样的数字,能从代码中得到验证不?于是我们查看源码找到了page大小的定义:

image.png

image.png

page大小为2^12=4096=4k个字节,其中开头56个字节存的是magic,next,thread,parent,child,depth,hiwat,第一页需要额外存一个哨兵对象指针占8字节,所以第一页能存(4096 - 56 - 8) / 8 = 504个对象,非第一页能存(4096 - 56) / 8 = 505个对象

ARC

image.png

obj方法的实现就是alloc

image.png

我们发现alloc生成的对象没有加入到自动释放池,而自定义方法obj生成的对象可以加入到自动释放池。 读者可以对方法名字进行探索,可以得到如下结论:使用allocnewcopymutableCopy等方法进行初始化时,由系统管理对象,在适当的位置release

对象的压栈

image.png

我们可以看到对象加入自动释放池的流程是objc_retainAutorelease-objc_autorelease-autorelease-rootAutorelease-rootAutorelease2-AutoreleasePoolPage::autorelease-autoreleaseFast

递归调用

运行以下代码测试是否可以递归执行@autoreleasepool

@autoreleasepool {
  SPObject *sp = [SPObject obj];
  NSLog(@"对象:%@",sp);
  @autoreleasepool {
    SPObject *sp2 = [SPObject obj];
    NSLog(@"对象2:%@",sp2);
    _objc_autoreleasePoolPrint();
  }
  _objc_autoreleasePoolPrint();
} 

image.png

结论是可以递归调用

总结

  • @autoreleasepoolllvm替换成__AtAutoreleasePool结构体代码,在进入作用域执行objc_autoreleasePoolPush,出作用域执行objc_autoreleasePoolPop
  • AutoreleasePoolPage的结构继承AutoreleasePoolPageData,其中的成员变量magic,next,thread,parent,child,depth,hiwat56个字节
  • 自动释放池的结构:
    • 线程相关,是个结构(先进后出),存的是8字节的指针
    • 存的指针是一个对象或者是一个boundary的哨兵边界
    • token指向边界用来释放池子
    • 双向链表结构
    • TSL存储hot
  • 第一页能存504个指针对象加一个哨兵,其他页可以存505个指针对象
  • ARC环境加入到自动释放池跟初始化方法名有关,allocnewcopymutableCopy等方法由系统管理对象,在适当的位置release
  • 可以递归调用