目前我们内存管理都在在ARC环境下进行自动管理对象的引用计数,我们下面看下autoreleasepool
。
1.autoreleasepool
1.1 release与autorelease
对象在调用release方法
的时候引用计数会立刻减1
。
而对象在调用autorelease
方法时,对象的引用计数会在稍后
的时间内减1,通常是在当前线程下一个“事件循环
”。autorelease方法
在方法返回对象的时候特别有用。如果方法中创建的对象在返回前
执行release方法
,那有可能对象还没有返回就被销毁
了。使用autorelease
方法,可以保证对象在返回的时候依然存活
,也无需调用者手动释放对象
,因为在当前线程的下一个事件循环
,对象的引用计数会自动减1.autorelease方法
可以延长对象的生命周期
,可以跨越方法调用边界
之后存活一段时间。
1.2 自动释放池
1、释放对象有两种方式,一种是调用release
方法,调用release
方法的对象引用计数会立刻递减
;另一种是调用autorelease
方法,将对象加入到“自动释放池
”中,自动释放池中的对象会在稍后的某个时候被释放
,清空自动释放池时
系统会向池中的对象发送release消息
。
2、自动释放池创建于左花括号
,并于右花括号处自动清空
,花括号范围内的对象,会于花括号的末尾处收到release消息
。
3、自动释放池可以降低应用程序的内存峰值
。
4、我们不需要担心自动释放池的创建问题
。主线程或者GCD创建的线程,默认都有自动释放池
,每一次执行事件循环
的时就会将其清空
1.3 Clang分析
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
复制代码
在main函数会把我们的代码加入autoreleasepool
中,Clang
结果如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2f_kt02d2w10p55r2z7mn5gc7qw0000gn_T_main_904163_mi_0);
}
return 0;
}
/*析构*/
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
复制代码
autoreleasepool
相当于一个对象,本质上。
__AtAutoreleasePool
是一个结构体,有构造函数 + 析构函数
,结构体定义的对象在作用域结束后,会自动调用析构函数- 其中
{}
是 作用域 ,优点是结构清晰,可读性强
,可以及时创建销毁
1.4 AutoreleasePoolPage
看下autoreleasepool
的源码描述
我们在clang的时候知道,自动释放池是一个析构函数, 构造的时候调用:objc_autoreleasePoolPush()
析构的时候调用objc_autoreleasePoolPop()
自动释放池调用是通过AutoreleasePoolPage
进行进行push
和pop
的。 其中页的大小为4096 AutoreleasePoolPage是一个class,有一些方法和自己的析构函数,对页的一些操作大致如下:
AutoreleasePoolPage
继承于AutoreleasePoolPageData
,我们看下
-
magic
用来校验 AutoreleasePoolPage 的结构是否完整; -
next
指向最新添加的 autoreleased 对象的下一个位置,初始化时指begin() ; -
thread
指向当前线程; -
parent
指向父结点,第一个结点的 parent 值为 nil ; -
child
指向子结点,最后一个结点的 child 值为 nil ; -
depth
代表深度,从 0 开始,往后递增 1; -
hiwat
代表 high water mark 最大入栈数量标记
发现其中有AutoreleasePoolPage
对象,所以有以下一个关系链AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage
,从这里可以说明自动释放池除了是一个页,还是一个双向链表
结构。
2. objc_autoreleasePoolPush
判断当前是否有释放池
,没有的话创建一个,有的话压栈一个POOL_BOUNDARY
.
- 继续看下
autoreleaseNewPage
- 查看
hotPage()
- 查看
autoreleaseNoPage
创建page
- 查看初始化自动释放池页
AutoreleasePoolPage
其中begin()
表示压入对象开始的位置,表示AutoreleasePoolPage
自己占有56
大小
- 查看自动释放池内存结构
在ARC环境下,无法手动调用autorelease
,将Demo切换至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting
设置为NO
)
我们for循环压入打印自动释放池情况
页的地址0x7fbcc2810000和边界地址0x7fbcc2810038相差16进制下3*16+8 =56,刚好是AutoreleasePoolPage
自己本身的内存大小。 我们加入505个对象
第一页存储了504
个对象,第二页存储了1
个对象,第二页不再存储边界哨兵
了。我们继续for循环1010个对象。 第一页 第二页
第三页
增加了505
个对象,增加了一页
,说明一页可以存储505个对象,大小为505*8 = 4040
大小,页本身大小为56
所以一页的大小为4096
和红定义一样。首页因为存储了哨兵对象
所以少存储一个对象
。
- 压栈对象
autoreleaseFast
进入autoreleaseFast
源码,主要有以下几步:
- 获取当前操作页,并判断页是否存在以及是否满了
- 如果页
存在,且未满
,则通过add
方法压栈对象 - 如果页
存在,且满了
,则通过autoreleaseFullPage
方法安排新的页面 - 如果
页不存在
,则通过autoreleaseNoPage
方法创建新页
autoreleaseFullPage
分析
遍历
当前页的子页面链,直到没有子页面创建
一个页面- 设置创建的页为
hotPage
操作页面 - 向创建的页添加对象,
压栈
。
autorelease
底层分析
autorelease->rootAutorelease->rootAutorelease2->autorelease(id obj)
最终还是执行autoreleaseFast
进行压栈操作,和压栈哨兵对象一样进入autoreleaseFast
只是对象不同。
3. objc_autoreleasePoolPop
在objc_autoreleasePoolPop
方法中有个参数,在clang分析时,发现传入的参数是push压栈后返回的哨兵对象
,即ctxt
,其目的是避免出栈混乱,防止将别的对象出栈
查看pop源码
- 空页面的处理,并
根据token获取page
- 容错处理
- 通过
popPage
出栈页
popPage
查看
不是哨兵对象的话通过popPage<false>(token, page, stop)
出栈操作,通过releaseUntil
出栈当前操作页面对象,释放到stop
位置之前的所有对象,之后对当前页进行判断是否为空,进行相关页kill
。
releaseUntil
查看
通过while循环遍历出当前stop的对象之前所有的object,哨兵除外进行objc_release
出栈操作,其中嵌套一个while循环,不断向前父节点页遍历。
进入releaseUntil
之后从后面的页,一页一页往前进行release页中的对象
,直到stop
对象,不是哨兵对象
就进行release
。
4. 总结
1.自动释放池主要是为我们管理对象内存
,不用我们手动的进行release
操作,而是由释放池在下次事件循环
时候把释放池中对象进行release
操作,会延缓
对象的生命周期
。
2. 自动释放池是一个对象
,自身有一个析构函数
,创建的时候进行压栈操作push
,析构
的时候进行出栈操作pop
。继承于autorelseasePoolPage
类,poolPage
本身继承于结构体autoreleasePoolPageData
,大小为56
字节。创建page后会插入一个POOL_Boundary
(哨兵对象)主要是为了防止越界
,但是只有首页会插入哨兵对象。一页最多可插入505
个对象,页的大小为505*8+56(页本身大小) = 4096字节
,poolPage是一个双向链表
的结构,它有一个父节点一个子节点,首页的父节点为空,末页的子节点为空。
3.autorelseasePoolPage
加入对象通过push
进行操作,没有页的话创建页同时传入哨兵
对象,这一步在构造
的时候就会调用。之后页满了的话通过autoreleaseNoPage
创建新的页,通过add
的方法加入对象。
4. 通常在析构
的时候调用pop
方法,会传入哨兵对象
,我们根据stop
的位置,释放stop对象之前所有
的对象,哨兵除外。通过while循环
当前页的---next
往上依次release
对象,当前页空
的话就把hotpage设置为父节点页面
,重复之前操作直到stop对象
。