首先这里先说一下RunLoop与自动释放池的关系吧
我是按照网上总结的RunLoop与自动释放池回答的面试官。当回答完这个以后,那么好接下来说一下自动释放池是怎么释放的释放时机是什么,我当时回答是在autoreleasepool{}花括号结束释放。这个回答不是面试官想要的答案, 我理解应该 内存管理引用计数器 的问题了吧?
autoreleased对象什么时候释放
下面通过3个例子来看一下到底什么时候释放的:
__weak NSString *string_A = nil;
__weak NSString *string_B = nil;
__weak NSString *string_C = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *StrA = [NSString stringWithFormat:@"-----场景1--------"];
string_A = StrA;
@autoreleasepool{
NSString *StrB = [NSString stringWithFormat:@"-----场景2--------"];
string_B = StrB;
}
NSString *StrC = nil;
@autoreleasepool{
StrC = [NSString stringWithFormat:@"-----场景3--------"];
string_C = StrC;
}
NSLog(@"******viewDidLoad*******%@",string_A);
NSLog(@"******viewDidLoad*******%@",string_B);
NSLog(@"******viewDidLoad*******%@",string_C);
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
NSLog(@"******viewWillAppear*******%@",string_A);
NSLog(@"******viewWillAppear*******%@",string_B);
NSLog(@"******viewWillAppear*******%@",string_C);
}
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSLog(@"******viewDidAppear*******%@",string_A);
NSLog(@"******viewDidAppear*******%@",string_B);
NSLog(@"******viewDidAppear*******%@",string_C);
}
2018-12-24 10:50:01.614715+0800 Autoreleasepool[24851:3003734] ******viewDidLoad*******-----场景1--------
2018-12-24 10:50:01.614845+0800 Autoreleasepool[24851:3003734] ******viewDidLoad*******(null)
2018-12-24 10:50:01.614964+0800 Autoreleasepool[24851:3003734] ******viewDidLoad*******-----场景3--------
2018-12-24 10:50:01.615169+0800 Autoreleasepool[24851:3003734] ******viewWillAppear*******-----场景1--------
2018-12-24 10:50:01.615303+0800 Autoreleasepool[24851:3003734] ******viewWillAppear*******(null)
2018-12-24 10:50:01.615390+0800 Autoreleasepool[24851:3003734] ******viewWillAppear*******(null)
2018-12-24 10:50:01.618742+0800 Autoreleasepool[24851:3003734] ******viewDidAppear*******(null)
2018-12-24 10:50:01.618871+0800 Autoreleasepool[24851:3003734] ******viewDidAppear*******(null)
2018-12-24 10:50:01.619000+0800 Autoreleasepool[24851:3003734] ******viewDidAppear*******(null)
- 当使用 [NSString stringWithFormat:@"-----场景1--------"] 创建一个对象时,这个对象的引用计数为 1(看一下这篇文章就清楚为啥是1了) ,并且这个对象被系统自动添加到了当前的 autoreleasepool 中。当使用局部变量 string 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。因为在 ARC 下 NSString *string 本质上就是 __strong NSString *string 。所以在 viewDidLoad 方法返回前,这个对象是一直存在的,且引用计数为 2 。而当 viewDidLoad 方法返回时,局部变量 string 被回收,指向了 nil 。因此,其所指向对象的引用计数 -1 ,变成了 1 。
- 场景2
当通过 [NSString stringWithFormat:@"-----场景2--------"] 创建一个对象时,这个对象的引用计数为 1 。而当使用局部变量 string 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。而出了当前作用域时,局部变量 string 变成了 nil ,所以其所指向对象的引用计数变成 1 。另外,我们知道当出了 @autoreleasepool {} 的作用域时,当前 autoreleasepool 被 drain ,其中的 autoreleased 对象被 release 。所以这个对象的引用计数变成了 0 ,对象最终被释放。 - 场景3
当出了 @autoreleasepool {} 的作用域时,其中的 autoreleased 对象被 release ,对象的引用计数变成 1 。当出了局部变量 string 的作用域,即 viewDidLoad 方法返回时,string 指向了 nil ,其所指向对象的引用计数变成 0 ,对象最终被释放。
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
Autorelease原理
AutoreleasePoolPage
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。
- AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
- AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
- AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:
图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。
所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
释放时刻
每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
- 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子: