前言
Autorelease
机制是iOS
开发者管理对象内存的好伙伴,MRC
中,调用[obj autorelease]
来延迟内存的释放是一件简单自然的事,ARC
下,我们甚至可以完全不知道Autorelease
就能管理好内存。而在这背后,objc
和编译器都帮我们做了哪些事呢,它们是如何协作来正确管理内存的呢?刨根问底,一起来探究下黑幕背后的Autorelease
机制。
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
对象的释放时机:
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
}
NSLog(@"%@", str); // Console: (null)
}
Autorelease原理
AutoreleasePoolPage
class AutoreleasePoolPage
{
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
//#define I386_PGBYTES 4096 /* bytes per 80386 page */
//#define PAGE_MAX_SIZE PAGE_SIZE
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
//magic用来校验AutoreleasePoolPage结构是否完整
magic_t const magic;
//next指向第一个可用的地址
id *next;
//thread指向当前的线程;
pthread_t const thread;
//parent指向父类
AutoreleasePoolPage * const parent;
//child指向子类
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
......
}
ARC
下,我们使用@autoreleasepool{}
来使用一个AutoreleasePool
,随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage
的简单封装,所以自动释放机制的核心就在于这个类。
AutoreleasePoolPage
是一个C++实现的类
-
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
(哨兵对象)作为入参,于是:
- 1、根据传入的哨兵对象地址找到哨兵对象所处的
page
- 2、在当前
page
中,将晚于哨兵对象插入的所有autorelease
对象都发送一次- release
消息,并向回移动next
指针到正确位置 - 3、补充2:从最新加入的对象一直向前清理,可以向前跨越若干个
page
,直到哨兵所在的page
刚才的objc_autoreleasePoolPop
执行后,最终变成了下面的样子:
嵌套的AutoreleasePool
知道了上面的原理,嵌套的AutoreleasePool
就非常简单了,pop
的时候总会释放到上次push
的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
Autorelease返回值的快速释放机制
值得一提的是,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
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编译的第三方库,或者反之)
只能动用更高级的黑魔法。
网友评论