autorelease机制是iOS管理对象内存的好伙伴,在MRC时代,我们通过[obj autorelease]
来延迟释放内存,到了ARC时代,我们甚至可以不用知道autorelease就可以管理好内存。那么,objc和编译器到底做了什么,帮助我们管理好内存呢?autorelease背后的机制到底是什么呢?
在说autorelease之前,大家可能需要大概了解下runloop相关的知识,纳尼?autorelease还跟runloop有关联?答案是肯定的,iOS很多内容都跟runloop有千丝万缕的关联,谁也离不开谁。app启动之后,苹果在主线程runloop里注册了两个observer,第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后,正因为这套机制的存在,我们不需要关心autorelease的存在。有兴趣的可以拜读下深入理解runloop这篇文章,反正我是已经读了好几遍了...
通过上述的简答,相信我们已经有了大致的了解,那么背后的机制到底是什么呢?下面我们来慢慢揭开。
Autorelease原理
AutoreleasePoolPage
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,
@autoreleasepool {
NSString *hello = @"hello world";
}
随后编译器将其改写成下面的样子:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSString *hello = (NSString *)&__NSConstantStringImpl__var_folders_g7_bldz1yr55832skz1w_v3ygj40000gn_T_ViewController_69a444_mi_0;
}
其中的关键就在于__AtAutoreleasePool
,然后我们找到相应的内容,这时我们发现了autorelease最核心的两个函数objc_autoreleasePoolPush
,objc_autoreleasePoolPop
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
那么上述的两个函数干了什么呢,通过苹果开源的源码
,我们定位到这两个函数其实是在操作AutoreleasePoolPage
这个类,其中AutoreleasePoolPage
定义如下
class AutoreleasePoolPage
{
#if PROTECT_AUTORELEASEPOOL
4096; // must be multiple of vm page size
#else
4096; // size and alignment, power of 2
#endif
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
通过这个类,我们可以看到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的位置),然后继续向栈顶添加新对象。
static inline void *push()
{
if (!hotPage()) {
setHotPage(new AutoreleasePoolPage(NULL));
}
id *dest = autoreleaseFast(POOL_SENTINEL);
assert(*dest == POOL_SENTINEL);
return dest;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else {
return autoreleaseSlow(obj);
}
}
id *add(id obj)
{
assert(!full());
unprotect();
*next++ = obj;
protect();
return next-1;
}
所以,向一个对象发送- autorelease
消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
释放时刻
每当进行一次objc_autoreleasePoolPush
调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象
,值为0
static inline void *push()
{
if (!hotPage()) {
setHotPage(new AutoreleasePoolPage(NULL));
}
id *dest = autoreleaseFast(POOL_SENTINEL); // POOL_SENTINEL = 0 返回哨兵对象的地址
assert(*dest == POOL_SENTINEL);
return dest;
}
那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush
的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)
作为入参,于是:
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次
- release
消息,并向回移动next
指针到正确位置 - 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
嵌套的AutoreleasePool
知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
参考资料:
https://blog.ibireme.com/2015/05/18/runloop/
https://blog.sunnyxx.com/2014/10/15/behind-autorelease/
https://opensource.apple.com/source/objc4/objc4-532/runtime/NSObject.mm.auto.html
网友评论