目录
- autorelease的本质
- autorelease对象什么时候释放?
- autoreleasepool的工作原理
- autoreleasepool的内部结构
- autoreleasepool的嵌套
- autoreleasePoolPage
- NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系
- 其他autorelease相关知识点
- 面试题
- 参考文章
autorelease的本质
- autorelease
本质
上就是延迟
调用release方法 - MRC环境,通过调用
[obj autorelease]
来延迟
内存的释放 - ARC环境,甚至可以
完全不知道
autorelease也能管理好内存
autorelease对象什么时候释放?
实验环境
- ARC
- 测试机型:
iPhone 4S模拟器
, -
注意:
在苹果一些新的硬件设备上,本实验的结果已经不再成立
__weak NSString *_weakStr = nil;
- (void)viewDidLoad
{
[super viewDidLoad];
// 场景 1
NSString *string = [NSString stringWithFormat:@"yanhoo"];
_weakStr = string;
// 场景 2
// @autoreleasepool {
// NSString *string = [NSString stringWithFormat:@"yanhoo"];
// _weakStr = string;
// }
// 场景 3
// NSString *string = nil;
// @autoreleasepool {
// string = [NSString stringWithFormat:@"yanhoo"];
// _weakStr = string;
// }
NSLog(@"string1: %@", _weakStr);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"string2: %@", _weakStr);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"string3: %@", _weakStr);
}
测试结果
场景1
2016-08-19 01:30:01.686 test[8866:554553] string1: yanhoo
2016-08-19 01:30:01.687 test[8866:554553] string2: yanhoo
2016-08-19 01:30:01.695 test[8866:554553] string3: (null)
场景2
2016-08-19 01:32:07.020 test[8886:556042] string1: (null)
2016-08-19 01:32:07.021 test[8886:556042] string2: (null)
2016-08-19 01:32:07.032 test[8886:556042] string3: (null)
场景3
2016-08-19 01:32:57.038 test[8900:557349] string1: yanhoo
2016-08-19 01:32:57.038 test[8900:557349] string2: (null)
2016-08-19 01:32:57.048 test[8900:557349] string3: (null)
分析
-
首先来了解下
__weak
修饰符-
不会
影响所指向对象的生命周期,即:使用__weak
修饰的变量不会
导致所引用的对象的引用计数+1 - 当
__weak
修饰的变量所指向的对象被释放时,__weak
修饰的变量的值会被置为nil
,不存在
野指针问题
-
-
场景1分析
- 当使用[NSString stringWithFormat:@"yanhoo"]创建一个
autorelease对象
时,这个对象的引用计数为1,并且这个对象被系统自动
添加到了最近
的autoreleasepool
中 - 当使用
局部变量string
(在ARC
下不指定变量所有权修饰符的情况下,默认为__strong
)指向这个对象时,这个对象的引用计数 +1 ,变成了 2 - 所以在
viewDidLoad
方法返回之前
,这个对象是一直存在的,且引用计数为2 - 当
viewDidLoad
方法返回之后
,局部变量 string被回收,其所指向对象的引用计数 -1 ,变成了1 - 在
viewWillAppear
方法中,我们仍然可以打印出这个对象的值,说明这个对象并没有被释放,说明到此autoreleasepool还未释放
,从而导致autorelease对象
未释放,因为只有当这个autoreleasepool
自身被drain
的时候,autoreleasepool
中的autoreleased 对象
才会被release
掉 - 在
viewDidAppear
中再打印这个对象的时候,对象的值变成了 nil,说明此时autoreleased对象
已经被释放了,可以大胆猜测autoreleasepool
一定在viewWillAppear
和viewDidAppear
方法之间的某个时候被drain
了
- 当使用[NSString stringWithFormat:@"yanhoo"]创建一个
场景2和场景3请读者自行分析,这里就不再啰嗦了
可以通过
lldb
的watchpoint
命令来观察,具体参考这篇文章-
总结
-
场景1
出现得最多,就是不需要我们手动添加@autoreleasepool {}
的情况,直接使用系统维护
的autoreleasepool
; -
场景2
就是需要我们手动添加@autoreleasepool {}
的情况,手动干预 autoreleased对象
的释放时机,在一些很耗内存的循环调用的场景下有时需要手动干预autoreleased 对象的释放时机,不然会导致内存暴增,最终导致程序奔溃; -
场景3
是为了区别于
场景2而引入的,在这种场景下并不能
达到出了@autoreleasepool {}
的作用域时autoreleased 对象
被释放的目的
-
autoreleasepool的工作原理
-
ARC
环境下,@autoreleasepool{ }
被编译器编译后,生成如下代码(以下代码是简化版
)
// push
void *poolToken = objc_autoreleasePoolPush();
// 这中间为写在{...}中的代码
// pop
objc_autoreleasePoolPop(poolToken);
- 在运行循环
开始前
,系统会自动创建
一个autoreleasepool
(一个
autoreleasepool会存在多个
AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush
函数,runtime会向当前的AutoreleasePoolPage
中add进一个POOL_SENTINEL
(哨兵对象
,值为0,也就是个nil,代表autoreleasepool的起始边界
),并返回此哨兵对象的内存地址poolToken
- 在运行循环
结束时
,autoreleasepool
会被drain
掉,此时会调用objc_autoreleasePoolPop(poolToken)
函数,入参是之前产生的POOL_SENTINEL
的内存地址poolToken
,对在POOL_SENTINEL之后
添加的所有autoreleased对象
调用一次release
,可以向前
跨越若干个page,直到哨兵对象
所在的page,并向回移动next指针
到哨兵对象所在位置
- 中间
{...}
所产生的autoreleased对象
都会被插入到最近
的autoreleasepool中(因为autoreleasepool存在嵌套
的情况) -
单个
autoreleasepool的运行过程可以简单地理解为以下三个过程- objc_autoreleasePoolPush()
- objc_autoreleasePoolPush()
本质
上就是调用的 AutoreleasePoolPage的push函数,如下所示
void * objc_autoreleasePoolPush(void) { if (UseGC) return nil; return AutoreleasePoolPage::push(); }
- 每执行一次
push
操作就会新建
一个autoreleasepool,对应的具体实现就是往AutoreleasePoolPage中的next位置
插入一个POOL_SENTINEL
,并且返回
插入的POOL_SENTINEL的内存地址poolToken
,这个地址在执行pop
操作的时候作为函数的入参,下面是AutoreleasePoolPage的push函数代码
static inline void *push() { id *dest = autoreleaseFast(POOL_SENTINEL); assert(*dest == POOL_SENTINEL); return dest; }
- push 函数通过调
autoreleaseFast
函数来执行具体的插入操作
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) {// 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置 return page->add(obj); } else if (page) {// 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中 return autoreleaseFullPage(obj, page); } else {// 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中 return autoreleaseNoPage(obj); } }
- objc_autoreleasePoolPush()
- [对象 autorelease]
- 本质上就是调用AutoreleasePoolPage的autorelease函数
__attribute__((noinline,used)) id objc_object::rootAutorelease2() { assert(!isTaggedPointer()); return AutoreleasePoolPage::autorelease((id)this); }
- AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较好理解了,它跟 push 操作的实现非常相似。只不过
push 操作
插入的是一个POOL_SENTINEL
,而autorelease 操作
插入的是一个具体的autoreleased 对象
static inline id autorelease(id obj) { assert(obj); assert(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); assert(!dest || *dest == obj); return obj; }
- objc_autoreleasePoolPop(poolToken)
- objc_autoreleasePoolPop(poolToken) 函数本质上也是调用的AutoreleasePoolPage的 pop 函数
void objc_autoreleasePoolPop(void *ctxt) { if (UseGC) return; // fixme rdar://9167170 if (!ctxt) return; AutoreleasePoolPage::pop(ctxt); }
- pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址
poolToken
。当执行pop
操作时,在POOL_SENTINEL内存地址在之后
添加的所有autoreleased 对象
都会被release
,可以向前
跨越若干个page,直到哨兵对象所在的page,并向回移动next指针
到哨兵对象所在位置
- objc_autoreleasePoolPush()
autoreleasepool的内部结构
- autoreleasepool
本质
上就是一个指针堆栈
-
指针堆栈
中存放的是autoreleased对象
的内存地址 或者POOL_SENTINEL
的内存地址 - 内部结构是由若干个
以page为结点的双向链表
组成,系统会在需要的时候动态地增加或删除
page节点,这里说的page就是下面即将说到的AutoreleasePoolPage
对象
autoreleasepool的嵌套
- 每产生
一个autoreleasePool
,就会产生一个哨兵对象
,作为pool的边界
- pool的
嵌套
其实就是产生多个哨兵对象
而已 - pop的时候可以
向前
跨越若干个page,直到指定哨兵对象
所在的page为止
AutoreleasePoolPage
-
一个
空的
AutoreleasePoolPage 的内存结构
如下图所示:-
magic
用来校验
AutoreleasePoolPage的结构是否完整
; -
next
指向下一个即将产生的autoreleased对象
的存放位置(当next == begin()
时,表示AutoreleasePoolPage为空
;当next == end()
时,表示AutoreleasePoolPage已满
) -
thread
指向当前线程
,一个
AutoreleasePoolPage只会对应一个线程
,但一个线程
可以对应多个
AutoreleasePoolPage; -
parent
指向父结点,第一个
结点的 parent 值为 nil; -
child
指向子结点,最后一个
结点的 child 值为 nil; -
depth
代表深度,第一个page的depth为0,往后每递增一个page,depth会加1; -
hiwat
代表 high water mark
-
前面所说的autoreleasepool的内部结构是由
若干个
以AutoreleasePoolPage
为结点的双向链表
组成,这个双向链表
就是通过上述结构中的parent指针
和child指针
连接起来的每个AutoreleasePoolPage对象会开辟
4KB
内存(也就是虚拟内存一页
的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autoreleased对象的内存地址
一个page的空间被占满时,会新建一个page,通过
parent指针
和child指针
连接链表,之后的autoreleased对象
会加入到新建的page中
NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系
- NSThread 和 NSRunLoop是
一一对应
的关系 - 在NSRunLoop对象的每个
运行循环(event loop)
开始前,系统会自动创建一个autoreleasepool,并在运行循环(event loop)
结束时drain
掉这个pool,同时释放所有autoreleased对象 -
autoreleasepool
只会对应一个
线程,每个线程
可能会对应多个
autoreleasepool,比如autoreleasepool嵌套
的情况
Autorelease返回值的快速释放机制
- ARC下,runtime有一套对autorelease返回值的优化策略
- 通过
objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
的配合使用可以达到最优化
程序运行的目的 -
Thread Local Storage(TLS)
线程局部存储,在返回值
返回前调用objc_autoreleaseReturnValue
方法时,runtime会将这个返回值
储存在TLS
中,然后直接返回返回值
(不调用
autorelease), - 在
外部接收
这个返回值
时通过调用objc_retainAutoreleasedReturnValue
发现TLS
中已存在这个返回值
,就直接返回(不调用retain
),免去了对返回值
的内存管理,达到优化目的
其他Autorelease相关知识点
- 使用容器的block版本的枚举器时,内部会
自动添加
一个autoreleasePool
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
{
// 这里被一个局部@autoreleasepool{ }包围着
}];
普通
for循环
和for in循环
中没有
,所以,还是新版的block版本枚举器更加方便,但是性能还是for in循环
最高-
下面三种情况是需要我们
手动
添加autoreleasepool- 如果你编写的程序
不是基于 UI 框架
的,比如:命令行工具; - for循环中遍历产生
大量
autorelease变量时,就需要手动
添加加局部
autoreleasePool来进行手动干预
- 如果你创建了一个
子线程
,一般会自定义继承自NSOperation
的操作,在main方法中要加上@autoreleasepool{...}
,这段代码是在子线程上执行是无法访问主线程
的自动释放池的,所以得自己创建
- (void)main { // 自己创建自动释放池,如果这段代码是在子线程上执行是无法访问主线程的自动释放池的,所以得自己创建 @autoreleasepool { // 代码逻辑 } }
- 如果你编写的程序
面试题
- autoreleasepool的实现原理