iOS 内存管理(自动释放池)

1,941 阅读3分钟

前言

Autorelease苹果文档

  • Autorelease pools提供了一个延迟向对象发送release消息的机制.它避免了当你想要放弃一个对象的所有权,但又不想马上被释放(比如当你从一个方法中返回一个对象)。

  • 比如当我们创建一个对象,并把它放入自动释放池,他不会立即被释放,会等到一次runloop结束或者作用域超出{}再被释放。

autoreleasepool 和runloop的关系

截屏2021-09-26 上午11.19.55.png

  • 主线程的NSRunLoop在监测到事件响应开启每一次event loop之前,会自动创建一个autorelease pool,并且会在event loop结束的时候执行drain操作,释放其中的对象
  • 如果我们的程序创建了大量临时变量,我们可以创建Autoreleasepool来降低内存峰值。

autoreleasepool和线程的关系

截屏2021-09-26 下午2.25.41.png

  • 每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构
  • 新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除
  • 对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池

clang分析

新建一个项目如下:

截屏2021-09-26 下午2.49.08.png 进入main.m 文件所在的目录,输入clang -rewrite-objc main.m -o main.cpp 会生成一个c++文件

截屏2021-09-26 下午2.57.19.png

截屏2021-09-26 下午2.56.46.png

  • @autoreleasepool底层是一个结构体
  • {}作用域内会调用objc_autoreleasePoolPush()构造函数和objc_autoreleasePoolPop()析构函数

继续添加一个objc_autoreleasePoolPush的符号断点:发现这个源码的实现在objc里面

截屏2021-09-26 下午3.06.03.png

autoreleasepool结构分析

打开objc源码,搜索objc_autoreleasePoolPush,

截屏2021-09-26 下午3.20.22.png 发现调用了AutoreleasePoolPage类里面的push方法,我们在分析源码的时候不要急于去看,我们应该先去了解底层结构.

结构分析

AutoreleasePoolPage继承自AutoreleasePoolPageData

截屏2021-09-26 下午3.30.30.png

  • magic:用来检测AutoreleasePoolPage的结构是否完整,占用16位
  • next:指向最新添加的autoreleased对象的下⼀个位置,初始化时指向begin(),占用8位
  • thread:指向当前线程 //占用8位
  • parent:指向⽗结点,第⼀个结点的parent值为nil 占用8位
  • child:指向⼦结点,最后⼀个结点的child值为nil 占用8位
  • depth:代表深度,从0开始,往后递增1 占用4位
  • hiwat:最大入栈标记数量 占用4位

通过注释理解结构

截屏2021-09-26 下午4.54.33.png

  • 一个线程的autorealse pool是一堆指针
  • 这个指针分两种一种是即将要释放的对象,一种是池的边界(POOL_BOUNDARY)。通过边界,我们可以知道page的对象是否被删除完。
  • 这个指针的集合结构实际上是一个双链表结构。pages之间互相关联.
  • 当前线程本地存储着指向新自动释放的热点页面对象

添加object流程

autoreleaseFast

如果是非bug模式会调用autoreleaseFast 截屏2021-09-26 下午5.10.31.png

autoreleaseFast调用 截屏2021-09-26 下午5.22.26.png

  • 如果page存储在并且没有满,直接在page后添加
  • 如果page满了调用autoreleaseFullPage
  • 如果没有page调用autoreleaseNoPage

autoreleaseNoPage流程

截屏2021-09-26 下午5.32.46.png

  • 新建一个page,设置为hotpage
  • page添加POOL_BOUNDARY边界
  • page添加obj对象 ,一个page可以看成一个单链表,page->add相当于在链表后面添加一个obj对象。

autoreleaseFullPage流程

截屏2021-09-26 下午5.41.36.png

  • 这个其实就是找到双链表的最后一个page,退出循环,page添加对应的obj

page的大小

在源码里面有一个宏定义 截屏2021-09-26 下午5.55.57.png 截屏2021-09-26 下午5.51.49.png

  • 所以一页page大小是1<<12,也就是4096.

  • 但是一页page能够存储多少个对象呢?,page本身属性大小 = magic+next+thread+parent+child+depth+hiwat = 56,如果是首页还有一个边界对象POOL_BOUNDARY占用8个字节。

  • 所以首个page,对象数量objcCount = (4096-56-8)/8 = 504

  • 如果不是首页没有边界,那就是505个对象

验证page大小

截屏2021-09-26 下午6.33.33.png _objc_autoreleasePoolPrint可以打印page的信息,上面生成了506个局部变量,打印结果如下

截屏2021-09-26 下午6.37.01.png

截屏2021-09-26 下午6.39.37.png

  • 总共有两页,第一页已经满了有504个对象,第二页有2个

AutoreleasePool的嵌套

很好奇如果AutoreleasePool嵌套,page的结构会是怎样的,做如下测试

截屏2021-09-26 下午7.43.23.png 查看打印结果:

截屏2021-09-26 下午7.46.29.png

截屏2021-09-26 下午7.46.13.png

  • 当第一个自动释放池的page没有放满的时候,嵌套的自动释放池的对象会继续往后面添加。
  • 只要创建一个自动释放池,就会创建一个边界对象。

自动释放池的释放

调用_objc_autoreleasePoolPop方法 截屏2021-09-26 下午8.34.55.png pop里面重点的方法就是调用popPage,其他的代码都是调试相关的,不影响主流程 截屏2021-09-26 下午8.54.49.png 进入popPage

截屏2021-09-26 下午8.57.21.png

releaseUntil

重点方法就是调用page->releaseUntil方法不断的调用objc_release(obj)给obj发送release消息。

截屏2021-09-26 下午8.59.31.png

截屏2021-09-26 下午9.21.34.png

  • 它的实现还是很容易的,用一个 while 循环持续释放 AutoreleasePoolPage 中的内容,直到 next 指向了 stop
  • 使用 memset 将内存的内容设置成 SCRIBBLE,然后使用 objc_release 释放对象。

总结

下面还用一张图来总结自动释放池,因为图形的记忆能够更加的深刻。 截屏2021-09-26 下午9.44.09.png