iOS-内存管理6-autorelease

一. 转成C++代码 我们都知道,在MRC中,当对象调用autorelease后,这个对象会在它所在的自动释放池结束后调用release方法,如下代码:

int main(int argc, const char * argv[]) { @autoreleasepool { MJPerson *person = [[[MJPerson alloc] init] autorelease]; } return 0; }

person指针指向的对象会在{}结束后调用release方法,但是它底层是怎么实现的呢?
将上面代码转成C++代码,如下:
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease")); } return 0; }

上面代码,相信应该很容易理解,剔除没用的,如下:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ...... }

上面定义了一个__autoreleasepool局部变量,搜索__AtAutoreleasePool定义,发现是个结构体,如下:
//C++的结构体和类很像,结构体中也可以定义函数,你可以认为它就是个类 struct __AtAutoreleasePool {//构造函数,在创建结构体的时候调用 __AtAutoreleasePool() { atautoreleasepoolobj = objc_autoreleasePoolPush(); }//析构函数,在结构体销毁的时候调用 ~__AtAutoreleasePool() { objc_autoreleasePoolPop(atautoreleasepoolobj); }void * atautoreleasepoolobj; };

根据上面代码,所以文章开头的代码其实就是这三行:
//构造函数 atautoreleasepoolobj = objc_autoreleasePoolPush(); //对象调用了autorelease MJPerson *person = [[[MJPerson alloc] init] autorelease]; //析构函数 objc_autoreleasePoolPop(atautoreleasepoolobj);

现在我们知道了,autoreleasepool会在刚开始调用Push,结束调用Pop,想要知道这两个函数内部做了什么还要进去看看。
在objc4里面搜索这两个函数:
void * objc_autoreleasePoolPush(void) { //调用C++类的push()方法 return AutoreleasePoolPage::push(); }void objc_autoreleasePoolPop(void *ctxt) { //调用C++类的pop()方法 AutoreleasePoolPage::pop(ctxt); }

可以发现,分别是调用AutoreleasePoolPage类的push()和pop()方法。
小总结:
  1. 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类
  2. 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
二. AutoreleasePoolPage类 进入AutoreleasePoolPage类,简化后留下有用的东西:
class AutoreleasePoolPage { ...... magic_t const magic; id *next; pthread_t const thread; //专属的线程 AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; ...... }

那么这些成员有什么用?
先看结论:
  1. 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址
  2. 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
什么是双向链表?
就是A链表可以访问B链表,B链表也可以访问A链表。
为什么要设计成双向链表?
因为一个AutoreleasePoolPage对象只占用4096字节内存,如果存满了,就会创建一个新的AutoreleasePoolPage对象,然后这些AutoreleasePoolPage对象之间通过通过双向链表的形式连接在一起,如下图:
iOS-内存管理6-autorelease
文章图片
双向链表.png
  1. 0x2000 - 0x1000 = 0x1000,转成10进制就是4096字节,一个AutoreleasePoolPage对象占用4096字节。
  2. 0x1038 - 0x1000 = 0x0038,转成10进制就是56字节,AutoreleasePoolPage对象内部的七个成员,每个成员占用8字节,所以一共占用56字节。
  3. 4096 - 56 = 4040,所以剩下的4040字节用来存放调用了autorelease方法的对象的地址,如果这个AutoreleasePoolPage对象里面不够存了,就会创建一个新的AutoreleasePoolPage对象。
上面的begin()函数是什么?同样在NSObject.mm里面找到源码:
id * begin() { return (id *) ((uint8_t *)this+sizeof(*this)); }id * end() { return (id *) ((uint8_t *)this+SIZE); }

对于begin(),就是this指针(就是自己的地址,如上面的0x1000)加上它自己有多大(就是他内部的七个成员变量的大小:56),返回的是一个地址,这个地址就是从什么地方开始存储调用了autorelease方法的对象的地址。
同理,对于end(),找到SIZE源码:
static size_t const SIZE = PAGE_MAX_SIZE; #define PAGE_MAX_SIZEPAGE_SIZE #define PAGE_SIZEI386_PGBYTES #define I386_PGBYTES4096

可以发现,SIZE就是4096字节,所以对于end(),就是自己地址加上4096,就得到结束的地方的地址。
parent指针指向上一个AutoreleasePoolPage对象的地址值,child指针指向下一个AutoreleasePoolPage对象的地址值。双向链表就是通过parent指针和child指针联系在一起的。
三. push()和pop() 那么push()和pop()函数里面究竟做了什么呢?
1. push() 调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,那么这句话是什么意思呢?
在objc4搜索POOL_BOUNDARY:
define POOL_BOUNDARY nil //就相当于0

看下图:
iOS-内存管理6-autorelease
文章图片
POOL_BOUNDARY.png 就是将0这个值存放到0x1038的位置,然后把0x1038这个地址值返回,如下代码返回的就是0x1038。
atautoreleasepoolobj = objc_autoreleasePoolPush(); // atautoreleasepoolobj = 0x1038

接下来就开始执行代码:
MJPerson *person = [[[MJPerson alloc] init] autorelease];

当发现有一个对象调用了autorelease,就把这个对象的地址值接着0x1038往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储,如下,当autoreleasepool里面有1000个对象的时候:
int main(int argc, const char * argv[]) { @autoreleasepool { for (int i = 0; i < 1000; i++) { MJPerson *person = [[[MJPerson alloc] init] autorelease]; } }

那么这1000个对象的地址在AutoreleasePoolPage对象里面存储的结构为:
iOS-内存管理6-autorelease
文章图片
1000个对象 当代码都执行完后,就会调用objc_autoreleasePoolPop()函数
2. pop() 【iOS-内存管理6-autorelease】调用pop()方法时传入一个POOL_BOUNDARY的内存地址(也就是上面说的0x1038),会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
上面的next是什么呢?
id *next指向了下一个能存放autorelease对象地址的区域。
比如,一开始next指向0x1038,当调用push之后把POOL_BOUNDARY入栈,这时候next就指向0x1038下一格的位置,如果这时候有一个对象的地址存储到了AutoreleasePoolPage对象里面,那么next就指向0x1038下下一格的位置,以此类推。
四. autoreleasepool嵌套 如下代码:
01 int main(int argc, const char * argv[]) { 02@autoreleasepool { //r1 = push() 03MJPerson *p1 = [[[MJPerson alloc] init] autorelease]; 04MJPerson *p2 = [[[MJPerson alloc] init] autorelease]; 05 06@autoreleasepool { // r2 = push() 07MJPerson *p3 = [[[MJPerson alloc] init] autorelease]; 08 09@autoreleasepool { // r3 = push() 10MJPerson *p4 = [[[MJPerson alloc] init] autorelease]; 11 12} // pop(r3) 13} // pop(r2) 14} // pop(r1) 15return 0; 16 }

第02行将POOL_BOUNDARY (r1)存进去
第03、04行分别将p1、p2存进去
第06行将POOL_BOUNDARY (r2)存进去
第07行将p3存进去
第09行将POOL_BOUNDARY (r3)存进去
第10行将p4存进去
第12行拿到r3,从最后一个进栈的对象(p4)开始release,一直到r3
第13行拿到r2,从最后一个进栈的对象(p3)开始release,一直到r2
第14行拿到r1,从最后一个进栈的对象(p2、p1)开始release,一直到r1
结合下图,更容易理解:
iOS-内存管理6-autorelease
文章图片
autoreleasepool嵌套 注意:
  1. 上面说的入栈并不是内存中的堆、栈那个栈,而是数据结构的那种栈。
  2. 我们知道栈是先进后出,比如上面的存储地址的过程,push进来和pop出去就达到了先进后出的效果。
使用打印验证: 以前说过,可以通过以下私有函数来查看自动释放池的情况:
extern void _objc_autoreleasePoolPrint(void);

_objc_autoreleasePoolPrint函数是私有的,使用extern声明这个函数,就可以直接调用了,如下:
extern void _objc_autoreleasePoolPrint(void); int main(int argc, const char * argv[]) { @autoreleasepool { //r1 = push() MJPerson *p1 = [[[MJPerson alloc] init] autorelease]; MJPerson *p2 = [[[MJPerson alloc] init] autorelease]; @autoreleasepool { // r2 = push() for (int i = 0; i < 600; i++) { MJPerson *p3 = [[[MJPerson alloc] init] autorelease]; }@autoreleasepool { // r3 = push() MJPerson *p4 = [[[MJPerson alloc] init] autorelease]; _objc_autoreleasePoolPrint(); //查看自动释放池的情况 } // pop(r3) } // pop(r2) } // pop(r1) return 0; }

打印:
objc[65684]: ############## objc[65684]: AUTORELEASE POOLS for thread 0x1000aa5c0//对应的线程 objc[65684]: 606 releases pending.//一共存了606个 objc[65684]: [0x101803000]................PAGE (full)(cold)//第一页 cold objc[65684]: [0x101803038]################POOL 0x101803038//POOL_BOUNDARY (r1) objc[65684]: [0x101803040]0x100541000MJPerson objc[65684]: [0x101803048]0x100541420MJPerson objc[65684]: [0x101803050]################POOL 0x101803050//POOL_BOUNDARY (r2) objc[65684]: [0x101803058]0x100540e10MJPerson objc[65684]: [0x101803ff0]0x10053bd10MJPerson ......省略 objc[65684]: [0x101802ef0]0x10053bd20MJPerson objc[65684]: [0x101803ff8]0x10053bd20MJPerson objc[65684]: [0x100806000]................PAGE(hot)//创建一个新的AutoreleasePoolPage对象第二页 hot objc[65684]: [0x100806038]0x10053bd30MJPerson objc[65684]: [0x100806040]0x10053bd40MJPerson ......省略 objc[65684]: [0x101803048]0x100541420MJPerson objc[65684]: [0x100806348]0x10053c350MJPerson objc[65684]: [0x100806350]################POOL 0x100806350//POOL_BOUNDARY (r3) objc[65684]: [0x100806358]0x10053c360MJPerson objc[65684]: ##############

一共存了606个,其中603个对象,3个POOL_BOUNDARY。从上面打印也可以看出,当AutoreleasePoolPage对象存不下时会创建一个新的AutoreleasePoolPage对象。其中第一页是cold是冷的意思,第二页是hot是热的意思,hot页是当前页的意思,以后release的时候就会从hot页开始。
五. 查看push()、autorelease、pop()源码 1. 先看push() 现在我们看push()和pop()函数的源码应该就很容易理解了:
static inline void *push() { id *dest; if (DebugPoolAllocation) { //没有page对象就new一个,并将POOL_BOUNDARY传进去 dest = autoreleaseNewPage(POOL_BOUNDARY); } else { //有page对象,直接将POOL_BOUNDARY传进去 dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }

进入autoreleaseFast函数:
static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { //如果有page并且没有满 return page->add(obj); //将POOL_BOUNDARY入栈 } else if (page) { //如果有page(满了) return autoreleaseFullPage(obj, page); } else { //如果没page return autoreleaseNoPage(obj); } }

通过上面两段push()的源代码可知,如果有page就直接将POOL_BOUNDARY入栈,如果没有page,就创建page之后再将POOL_BOUNDARY入栈,验证了我们上面说的。
2. 再看autorelease 在NSObjec.mm -> autorelease -> rootAutorelease -> rootAutorelease2,进入rootAutorelease2函数:
objc_object::rootAutorelease2() { assert(!isTaggedPointer()); //哪一个对象调用autorelease就将哪一个对象传进去,并调用AutoreleasePoolPage的autorelease方法 return AutoreleasePoolPage::autorelease((id)this); }

可以看出,哪一个对象调用autorelease就将哪一个对象传进去,并调用AutoreleasePoolPage的autorelease方法,进入autorelease方法:
static inline id autorelease(id obj) { assert(obj); assert(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); //将对象地址值add到AutoreleasePoolPage里面去 assert(!dest||dest == EMPTY_POOL_PLACEHOLDER||*dest == obj); return obj; }

可以发现,这里也调用了autoreleaseFast函数,autoreleaseFast函数的实现上面有,就是将对象地址值add到AutoreleasePoolPage里面去。
3. 再看pop()
static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; ......省略page = pageForPointer(token); stop = (id *)token; //token就是POOL_BOUNDARY的地址值,将token赋值给stop if (*stop != POOL_BOUNDARY) { if (stop == page->begin()&&!page->parent) { } else { return badPop(token); } }if (PrintPoolHiwat) printHiwat(); page->releaseUntil(stop); //释放对象,直到遇到stop为止......省略 }

省略掉其他代码,只看上面的注释。
pop()函数需要传入一个参数,这个参数就是POOL_BOUNDARY的地址值,最后调用releaseUntil释放对象,直到遇到stop为止,进入releaseUntil函数:
void releaseUntil(id *stop) { //使用while循环不断取出page里面的东西 while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); while (page->empty()) { page = page->parent; setHotPage(page); }page->unprotect(); id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); //将取出的东西release掉 if (obj != POOL_BOUNDARY) { objc_release(obj); } }

上面代码使用while循环不断取出page里面存储的对象,然后将取出的对象release掉,和我们上面讲的一样。
六. 总结 下面代码:
@autoreleasepool { MJPerson *person = [[[MJPerson alloc] init] autorelease]; }

底层就是下面三行:
//构造函数 atautoreleasepoolobj = objc_autoreleasePoolPush(); //对象调用了autorelease MJPerson *person = [[[MJPerson alloc] init] autorelease]; //析构函数 objc_autoreleasePoolPop(atautoreleasepoolobj);

  1. 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类,调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
  2. 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址,所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
  3. 调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。
  4. 当发现有一个对象调用了autorelease,就把这个对象的地址值接着POOL_BOUNDARY往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储。
  5. 调用pop()方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
七. RunLoop与autorelease 1. 面试题1 调用autorelease的对象在什么时机会被调用release?
① 如果有@autoreleasepool{} 创建一个新项目,修改为MRC,代码如下:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"111"); @autoreleasepool { MJPerson *person = [[[MJPerson alloc] init] autorelease]; } NSLog(@"333"); }

打印:
111 -[MJPerson dealloc] 333

根据我们上面学的知识,很好理解,因为使用了autoreleasepool,所以autoreleasepool里面调用了autorelease方法的对象会在{}结束之后释放,所以才是上面打印。
② 如果没写@autoreleasepool{} 那如果没写@autoreleasepool呢?
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"111"); MJPerson *person = [[[MJPerson alloc] init] autorelease]; NSLog(@"333"); }

打印:
111 333 -[MJPerson dealloc]

你可能会想,上面的代码没autoreleasepool,但是整个程序的main函数里面不是有一个autoreleasepool吗,那上面那些代码是不是被main函数的autoreleasepool管理呢?
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }

显然不是的,如果上面的代码是被main函数的autoreleasepool管理的,那么程序退出之前这个autoreleasepool是不会结束的,对象就不会被释放,但是上面打印的结果表明对象的确被释放了,说明上面那些代码不是被main函数的autoreleasepool管理的。
可能你还会想,那person对象会不会是在viewDidLoad方法调用完毕再释放的呢?
这个更好验证:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"111"); MJPerson *person = [[[MJPerson alloc] init] autorelease]; NSLog(@"333"); NSLog(@"%s", __func__); }- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"%s", __func__); }- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"%s", __func__); }

打印:
111 333 -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[MJPerson dealloc] -[ViewController viewDidAppear:]

可以发现,是在viewWillAppear之后才释放的,可能你会越来越迷糊,那person对象究竟是在什么时候被释放呢?
其实这个问题和RunLoop有关:
打印NSLog(@"%@",[NSRunLoop mainRunLoop]),打印结果比较多,抽取我们需要的两个监听器,如下:
observers = ( "{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10d690c9d), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7ffc78003058>\n)}}", ......省略 "{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10d690c9d), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7ffc78003058>\n)}}" ),

RunLoop的状态如下,下面会用到
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), 1 kCFRunLoopBeforeTimers = (1UL << 1), 2 kCFRunLoopBeforeSources = (1UL << 2), 4 kCFRunLoopBeforeWaiting = (1UL << 5), 32 kCFRunLoopAfterWaiting = (1UL << 6), 64 kCFRunLoopExit = (1UL << 7), 128 kCFRunLoopAllActivities = 0x0FFFFFFFU };

观察打印结果里面的activities
第一个observer的activities = 0x1,就是1,说明第一个observer监听kCFRunLoopEntry状态。
第一个observer的activities = 0xa0,转成十进制是160,正好是32+128,说明第二个observer监听kCFRunLoopBeforeWaiting和kCFRunLoopExit状态。
看着MJ老师的图,我们进行总结:
RunLoop的运行逻辑.png 总结:
  1. iOS在主线程的Runloop中注册了2个Observer,当第1个Observer监听到了进入状态(kCFRunLoopEntry),就会调用objc_autoreleasePoolPush()
  2. 当第2个Observer监听到了即将休眠状态(kCFRunLoopBeforeWaiting)就会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()
  3. 当第2个Observer监听到了即将退出状态(kCFRunLoopBeforeExit)就会调用objc_autoreleasePoolPop()
这样,整个RunLoop运行循环中push和pop就能完全对得上。
现在就能回答刚才的问题了,如果没写@autoreleasepool{},由于整个程序没有退出,autoreleasepool里面调用了autorelease方法的对象会在RunLoop休眠之前被释放。
- (void)viewDidLoad { [super viewDidLoad]; // 这个Person什么时候调用release,是由RunLoop来控制的 // 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release MJPerson *person = [[[MJPerson alloc] init] autorelease]; }

再次观察上面的打印信息:
111 333 -[ViewController viewDidLoad] -[ViewController viewWillAppear:] -[MJPerson dealloc] -[ViewController viewDidAppear:]

既然person对象会在RunLoop休眠之前被释放,那么可以看出viewDidLoad和viewWillAppear处在同一次运行循环中(因为一次休眠到下一次休眠是一个循环)。
2. 面试题2 ARC中,方法里有局部对象,出了方法后会立即释放吗?
这个问题,我们猜想有两种可能:
  1. 如果ARC生成的代码是直接在方法完成之前给对象调用了一次[person release],那么对象就会在方法结束之后立马释放。
  2. 如果ARC生成的代码是直接在对象后面加autorelease,那么对象就会在RunLoop休眠之前被释放。
我们实验一下,运行如下代码:
- (void)viewDidLoad { [super viewDidLoad]; MJPerson *person = [[MJPerson alloc] init]; NSLog(@"%s", __func__); // ARC中就相当于在这里生成一行 [person release]; }- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"%s", __func__); }- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"%s", __func__); }

打印:
-[ViewController viewDidLoad] -[MJPerson dealloc] -[ViewController viewWillAppear:] -[ViewController viewDidAppear:]

可以发现,viewDidLoad执行完后对象立马就被释放了,说明ARC中,方法里有局部对象,出了方法后会立即释放,因为就相当于在方法的最后加一行release代码。
Demo地址:autorelease

    推荐阅读