autoReleasePool
# autorelease的本质
autorelease本质就是延迟调用release方法 MRC环境下,通过[obj autorelease]来延迟内存的释放 ARC环境下,是不能手动调用,系统会自动给对象添加autorelease
# autoreleasePoolPage的结构
class AutoreleasePoolPage
{
PAGE_MAX_SIZE;//最大size 4096字节
magic_t const magic; //用来校验AutoreleasePoolPage的结构是否完整
id *next;//指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满
pthread_t const thread;//指向当前线程,一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage;
AutoreleasePoolPage * const parent;//指向父结点,第一个结点的 parent 值为 nil;
AutoreleasePoolPage *child;//指向子结点,最后一个结点的 child 值为 nil;
uint32_t const depth;//代表深度,第一个page的depth为0,往后每递增一个page,depth会加1;
}
2
3
4
5
6
7
8
9
10
1.AutoreleasePoolPage 本质是这么一个节点对象,大小是4096字(PAGE_MAX_SIZE:4096)。
2.前7个变量都是8字节,所以总共占用56个字节,剩下的4040字节存储着autorelease对象地址
提出个小小疑问,大家有没有想过,为什么每个AutoreleasePoolPage的大小设置成4096个字节呢? 因为4096是虚拟内存一页的大小
# autoreleasePool的结构和工作原理
autoreleasepool本质上就是一个指针堆栈,内部结构是由若干个以AutoreleasePoolPage对象为结点的双向链表组成,系统会在需要的时候动态地增加或删除page节点,如下图即为AutoreleasePoolPage组成的双向链表:
# 工作流程
在运行循环开始前,系统会自动创建一个autoreleasepool(一个autoreleasepool会存在多个AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush函数,runtime会向当前的AutoreleasePoolPage中add进一个POOL_BOUNDARY(哨兵对象),代表autoreleasepool的起始边界地址),并返回此哨兵对象的内存地址。
这时候next指针则会指向POOL_BOUNDARY(哨兵对象)后面的地址(对象地址1)。
后面我们创建对象,如果对象调用了autorelease方法(ARC编译器会给对象自动插入autorelease),则会被添加进AutoreleasePoolPage中,位置是在next指针指向的位置,如上面next指向的是对象地址1,这是后添加的对象地址就在对象地址1这里,然后next就会 指向到对象地址2 ,以此类推,每添加一个地址就会向前移动一次,直到指向end()表示已存满。
当不断的创建对象时,AutoreleasePoolPage不断存储对象地址,直到存满后,则又会创建一个新的AutoreleasePoolPage,使用child指针和parent指针指向下一个和上一个page,从而形成一个双向链表,对象地址存储的顺序如图所示。
当调用objc_autoreleasePoolPop(哨兵对象地址)时(调用时机后面说),假设我们如上图,添加最后一个对象地址8,那么这时候就会依次由对象地址8 -> 对象地址1,每个对象都会调用release方法释放,直到遇到哨兵对象地址为止。
_objc_autoreleasePoolPush()和 _objc_autoreleasePoolPop的调用时机
App启动后主线程的RunLoop注册两个observer:_warpRunLoopWithAutoreleasePoolHandle()
第一个observer监听事件Entry,回调调用_objc_autoreleasePoolPush()创建自动释放池,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个observer监听两个事件:
1. Before waiting时调用_objc_autoreleasePoolPop和push清空自动释放池。
2. Exit时调用_objc_autoreleasePoolPop释放清空。
优先级最低,保证其释放池子发生在其他所有回调之后。
# autorelaeasepool、NSRunLoop 、子线程三者的关系
主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理。
子线程默认不开启runloop,当产生autorelease对象时候,会将对象添加到最近一次创建的autoreleasepool中,一般是main函数中的autoreleasepool,由主线程runloop管理;也就是不用手动创建Autoreleasepool,线程销毁时在会在最近一次创建的autoreleasepool 中释放对象。
自定义的 NSOperation 和 NSThread 需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。
AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程),每开一个线程,会有与之对应的AutoreleasePool。
# 如何实现autorealeasepool
autorealeasepool(自动释放池)其实并没有其自身的结构,它是基于多个Autorelease PoolPage(一个C++类)以双向链表组合起来的结构,其基本操作都是简单封装了AutoreleasePoolPage的操作方法。例如,可以通过push操作添加对象,或者通过pop操作弹出对象,以及通过release操作释放销毁对象,对应的3个封装后的操作函数为:objc_autoreleasepoolPush、objc_autoreleasepoolPop和objc_autorelease。自动释放池将用完的对象集中起来,统一释放,起到延迟释放对象的作用。
自动释放池存储于内存中的栈上,释放池之间遵循“先进后出”原则。例如下面代码所示的释放池嵌套。
提示
/* 释放池 1*/
@autoreleasepool {
People *person1 = [[[Person alloc] init] autorelease];
/* 释放池 2*/
@autoreleasepool {
People *person2 = [[[Person alloc] init] autorelease];
}
People *person3 = [[[Person alloc] init] autorelease];
}
2
3
4
5
6
7
8
9
代码中释放池1和释放池2在内存中的结构如图所示,释放池1先入栈,后出栈;释放池2后入栈,先出栈。person2对象在释放池2中,会被先释放;person1和person3在释放池1中,会后被释放。

常见真题:下面这段代码有什么问题?如何修改?
for (int i = 0; i < someLargeNumber; i++) {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@",string);
}
2
3
4
5
6
答案:代码通过循环短时间内创建了大量的NSString对象,在默认的自动释放池释放之前这些对象无法被立即释放,会占用大量内存,造成内存高峰以致内存不足。 为了防止大量对象堆积应该在循环内手动添加自动释放池,这样在每一次循环结束,循环内的自动释放池都会被自动释放及时腾出内存,从而大大缩短了循环内对象的生命周期,避免内存占用高峰。 代码改进方法是在循环内部嵌套一个自动释放池:
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *string = @"Abc";
string = [string lowercaseString];
string = [string stringByAppendingString:@"xyz"];
NSLog(@"%@",string);
}
}
2
3
4
5
6
7
8
# 相关问题
# ARC 下什么样的对象由 Autoreleasepool 管理
__weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象可能被废弃。那么如果把对象注册到autorealeasepool中,那么在@autorealeasepool块结束之前都能确保对象的存在。最新的情况是weak修饰的对象不会再被加入到Pool了
# 子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage,如果你还是不理解,可以先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用 page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!StackOverFlow 的作者也说道,这个是 OS X 10.9+和 iOS 7+ 才加入的特性。
# 子线程是否有自动释放池呢
在子线程中原本是没有自动释放池的,但是如果有runloop或者autorelease对象的时候,就会自动的创建自动释放池。
# 这个obj究竟在什么时候释放呢? 是在子线程销毁后释放,还是与子线程的生命周期无关?
每个autorelease创建的时候都会监听当前线程的销毁方法,在线程退出时调用tls_dealloc方法。
# 什么时候需要自己手动创建autorelease pool
你写的循环创建了大量临时对象 -> 你需要在循环体内创建一个autorelease pool block并且在每次循环结束之前处理那些autoreleased对象. 在循环中使用autorelease pool block可以降低内存峰值。
解析
该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,在循环中创建自己的autoReleasePool,能够及时释放占用内存大的临时变量,减少内存占用峰值
我们经常会在一些第三方库中看到这种情况,线程的入口加上Autorelease Pool,这是为何?
一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
在默认情况下线程是joinable,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
由此可知,线程占用的资源要释放的前提是线程终止,如果加了autoreleasepool相关对象会在pool执行完毕后释放,避免过多的延迟释放造成程序占用过多的内存。
# 如果一个对象释放前被加到了NotificationCenter中,不在NotificationCenter中,那么remove对象可能会怎样
前面已经讲到对于NotificationCenter的使用,只要添加对象到消息中心进行通知注册,之后就一定要对其remove进行通知注销。将对象添加到消息中心后,消息中心只是保存该对象的地址,消息中心到时候会根据地址发送通知给该对象,但并没有取得该对象的强引用,对象的引用计数不会加1。如果对象释放后没有从消息中心remove,也就是通知中心还保存着那个指针,而那个指针指的对象可能已经被释放销毁了,那么那个指针就成为一个野指针,当通知发生时,会向这个野指针发送消息导致程序崩溃。
# 关联对象的应用?系统如何实现关联对象的
应用: 可以在不改变类的源码的情况下,为类添加实例变量(注意:这里指的实例变量,并不是真正的属于类的实例变量,而是一个关联值变量) 结合category使用,为类扩展存储属性。 关联对象实现原理:
系统通过管理一个全局哈希表,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter传入的参数协议,来管理对象的生命周期。
关联对象的值实际上是通过AssociationsManager对象负责管理的,这个对象里有个AssociationsHashMap静态表,用来存储对象的关联值的,关于AssociationsHashMap存储的数据结构如下:
AssociationsHashMap:
------添加属性对象的指针地址(key):ObjectAssociationMap(value:所有关联值对象)
ObjectAssociationMap:
------关联值的key:关联值的value
# objc中向一个nil对象发送消息将会发生什么?
如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。
详解: 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil); 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0; 如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0; 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的