自动释放池-AutoReleasePool
自动释放池是OC中的一种内存自动回收机制,它可以将加入AutoreleasePool中的变量release的时机延迟,简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到runloop休眠/超出autoreleasepool作用域{}之后才会被释放
1、从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop
2、用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等
3、runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
4、在一次完整的runloop结束之前,会向自动释放池中所有对象发送release消息,然后销毁自动释放池
总结
autoreleasepool其本质是一个结构体对象,一个自动释放池对象就是页,是是栈结构存储,符合先进后出的原则即可
页的栈底是一个56字节大小的空占位符,一页总大小为4096字节
只有第一页有哨兵对象,最多存储504个对象,从第二页开始最多存储505个对象
autoreleasepool在加入要释放的对象时,底层调用的是objc_autoreleasePoolPush方法
-
autoreleasepool在调用析构函数释放时,内部的实现是调用objc_autoreleasePoolPop方法
对于自动释放池,我们主要关心的点有以下三点:
1、自动释放池什么时候创建?
2、对象是如何加入自动释放池的?
3、哪些对象才会加入自动释放池?
objc_autoreleasePoolPush 源码分析
判断是否为有 pool
如果没有,则通过autoreleaseNewPage方法创建
-
如果有,则通过autoreleaseFast压栈哨兵对象
1、创建页 autoreleaseNewPage
进入objc_autoreleasePoolPush -> push -> autoreleaseNewPage源码实现,主要是通过hotPage`获取当前页,判断当前页是否存在
如果存在,则通过autoreleaseFullPage方法压栈对象
-
如果不存在,则通过autoreleaseNoPage方法创建页
2、压栈对象 autoreleaseFast
进入autoreleaseFast源码,主要有以下几步:
获取当前操作页,并判断页是否存在以及是否满了
如果页存在,且未满,则通过add方法压栈对象
如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面
-
如果页不存在,则通过autoreleaseNoPage方法创建新页
3、autorelease 底层分析
如果是对象,则调用对象的autorelease进行释放
如果不是对象 或者 是小对象,则直接返回
objc_autoreleasePoolPop 源码分析
进入pop源码实现,主要由以下几步
空页面的处理,并根据token获取page
容错处理
通过popPage出栈页
-
进入releaseUntil实现,主要是通过循环遍历,判断对象是否等于stop,其目的是释放stop之前的所有的对象,
首先通过获取page的next释放对象(即page的最后一个对象),并对next进行递减,获取上一个对象
-
判断是否是哨兵对象,如果不是则自动调用objc_release释放
进入kill实现,主要是销毁当前页,将当前页赋值为父节点页,并将父节点页的child对象指针置为nil
总结
在自动释放池的压栈(即push)操作中
当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象
在页中压栈普通对象主要是通过next指针递增进行的,
-
当页满了时,需要设置页的child对象为新建页
在自动释放池的出栈(即pop)操作中
在页中出栈普通对象主要是通过next指针递减进行的,
-
当页空了时,需要赋值页的parent对象为当前页
相关面试题
AutoreleasePool 相关
面试题1:临时变量什么时候释放?
- 如果在正常情况下,一般是超出其作用域就会立即释放
- 如果将临时变量加入了自动释放池,会延迟释放,即在runloop休眠或者autoreleasepool作用域之后释放 面试题2:AutoreleasePool原理
- 自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接
- 自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPush和objc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法
- 每次调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个POOL_BOUNDARY,并返回插入POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况
- 当page存在,且不满时,调用add方法将对象添加至page的next指针处,并next递增
- 当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中
- 当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中
- 当执行pop操作时,会传入一个值,这个值就是push操作的返回值,即POOL_BOUNDARY的内存地址token。所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置 面试题3:AutoreleasePool能否嵌套使用?
- 可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高
- 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的
- 自动释放池的多层嵌套其实就是不停的pushs哨兵对象,在pop时,会先释放里面的,在释放外面的 面试题4:哪些对象可以加入AutoreleasePool?alloc创建可以吗?
- 使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中
- 设置为autorelease的对象不需要手动释放,会直接进入自动释放池
- 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中 面试题5:AutoreleasePool的释放时机是什么时候?
- App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创 建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即 将退出 Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。 面试题6:thread 和 AutoreleasePool的关系
- 每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构
- 新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除
- 对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池
总结:每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池 面试题7:RunLoop 和 AutoreleasePool的关系 - 主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool
- 并且会在事件循环结束时,执行drain操作,释放其中的对象