Autorelease机制是iOS开发者管理对象内存的好伙伴,MRC中,调用[obj autorelease]
来延迟内存的释放是一件简单天然的事,ARC下,咱们甚至能够彻底不知道Autorelease就能管理好内存。而在这背后,objc和编译器都帮咱们作了哪些事呢,它们是如何协做来正确管理内存的呢?刨根问底,一块儿来探究下黑幕背后的Autorelease机制。原文连接面试
这个问题拿来作面试题,问过不少人,没有几个能答对的。不少答案都是“当前做用域大括号结束时释放”,显然木有正确理解Autorelease机制。
在没有手加Autorelease Pool的状况下,Autorelease对象是在当前的runloop
迭代结束时释放的,而它可以释放的缘由是系统在每一个runloop迭代中都加入了自动释放池Push和Pop 架构
__weak id reference = nil; - (void)viewDidLoad { [super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"sunnyxx"]; // str是一个autorelease对象,设置一个weak的引用来观察它 reference = str; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"%@", reference); // Console: sunnyxx } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"%@", reference); // Console: (null) }
这个实验同时也证实了viewDidLoad
和viewWillAppear
是在同一个runloop调用的,而viewDidAppear
是在以后的某个runloop调用的。
因为这个vc在loadView以后便add到了window层级上,因此viewDidLoad
和viewWillAppear
是在同一个runloop调用的,所以在viewWillAppear
中,这个autorelease的变量依然有值。 函数
固然,咱们也能够手动干预Autorelease对象的释放时机: oop
- (void)viewDidLoad { [super viewDidLoad]; @autoreleasepool { NSString *str = [NSString stringWithFormat:@"sunnyxx"]; } NSLog(@"%@", str); // Console: (null) }
ARC下,咱们使用@autoreleasepool{}
来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:优化
void *context = objc_autoreleasePoolPush(); // {}中的代码 objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage
的简单封装,因此自动释放机制的核心就在于这个类。ui
AutoreleasePoolPage是一个C++实现的类spa
双向链表
的形式组合而成(分别对应结构中的parent指针和child指针)id *next
指针做为游标指向栈顶最新add进来的autorelease对象的下一个位置因此,若当前线程中只有一个AutoreleasePoolPage对象,并记录了不少autorelease对象地址时内存以下图:线程
图中的状况,这一页再加入一个autorelease对象就要满了(也就是next指针立刻指向栈顶),这时就要执行上面说的操做,创建下一页page对象,与这一页链表链接完成后,新page的next
指针被初始化在栈底(begin的位置),而后继续向栈顶添加新对象。指针
因此,向一个对象发送- autorelease
消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置code
每当进行一次objc_autoreleasePoolPush
调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象
,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush
的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)
做为入参,因而:
- release
消息,并向回移动next
指针到正确位置刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
知道了上面的原理,嵌套的AutoreleasePool就很是简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱同样,每次一层,互不影响。
【附加内容】
值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略。
好比一个工厂方法:
+ (instancetype)createSark { return [self new]; } // caller Sark *sark = [Sark createSark];
秉着谁建立谁释放的原则,返回值须要是一个autorelease对象才能配合调用方正确管理内存,因而乎编译器改写成了形以下面的代码:
+ (instancetype)createSark { id tmp = [self new]; return objc_autoreleaseReturnValue(tmp); // 代替咱们调用autorelease } // caller id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替咱们调用retain Sark *sark = tmp; objc_storeStrong(&sark, nil); // 至关于代替咱们调用了release
一切看上去都很好,不过既然编译器知道了这么多信息,干吗还要劳烦autorelease这个开销不小的机制呢?因而乎,runtime使用了一些黑魔法将这个问题解决了。
Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存做为某个线程专有的存储,以key-value的形式进行读写,好比在非arm架构下,使用pthread提供的方法实现:
void* pthread_getspecific(pthread_key_t); int pthread_setspecific(pthread_key_t , const void *);
说它是黑魔法可能被懂pthread的笑话- -
在返回值身上调用objc_autoreleaseReturnValue
方法时,runtime将这个返回值object储存在TLS中,而后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue
里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
因而乎,调用方和被调方利用TLS作中转,颇有默契的免去了对返回值的内存管理。
因而问题又来了,假如被调方和主调方只有一边是ARC环境编译的该咋办?(好比咱们在ARC环境下用了非ARC编译的第三方库,或者反之)
只能动用更高级的黑魔法。
这个内建函数原型是char *__builtin_return_address(int level)
,做用是获得函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。
- (int)foo { NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址 return 1; } // caller int ret = [sark foo];
看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束此次调用的地址(或者相差某个固定的偏移量,根据编译器决定)
也就是说,被调用的函数也有翻身作地主的机会了,能够反过来对主调方干点坏事。
回到上面的问题,若是一个函数返回前知道调用方是ARC仍是非ARC,就有机会对于不一样状况作不一样的处理
经过上面的__builtin_return_address加某些偏移量,被调方能够定位到主调方在返回值后面的汇编指令
:
// caller int ret = [sark foo]; // 内存中接下来的汇编指令(x86,我不懂汇编,瞎写的) movq ??? ??? callq ???
而这些汇编指令在内存中的值是固定的,好比movq对应着0x48。
因而乎,就有了下面的这个函数,入参是调用方__builtin_return_address传入值
static bool callerAcceptsFastAutorelease(const void * const ra0) { const uint8_t *ra1 = (const uint8_t *)ra0; const uint16_t *ra2; const uint32_t *ra4 = (const uint32_t *)ra1; const void **sym; // 48 89 c7 movq %rax,%rdi // e8 callq symbol if (*ra4 != 0xe8c78948) { return false; } ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l; ra2 = (const uint16_t *)ra1; // ff 25 jmpq *symbol@DYLDMAGIC(%rip) if (*ra2 != 0x25ff) { return false; } ra1 += 6l + (long)*(const int32_t *)(ra1 + 2); sym = (const void **)ra1; if (*sym != objc_retainAutoreleasedReturnValue) { return false; } return true; }
它检验了主调方在返回值以后是否紧接着调用了objc_retainAutoreleasedReturnValue
,若是是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。
使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { // 这里被一个局部@autoreleasepool包围着 }];
固然,在普通for循环和for in循环中没有,因此,仍是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就须要手加局部AutoreleasePool咯