开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
一、AutoreleasePool初探
我们先看一个例子代码:
@autoreleasepool {
// insert code here...
NSLog(@"Hello world!");
NYPerson *p = [NYPerson alloc];
_objc_autoreleasePoolPrint();//查看自动释放池
}
运行查看打印结果:
在打印中没有看到NYPerson的内存信息。
如果我们加上 __autoreleasing NYPerson *p = [NYPerson alloc]; 在看下打印:
现在我们看到了NYPerson的内存信息,NYPerson对象被添加到自动释放池中。
为什么不添加__autoreleasing关键字,不会添加到autoreleasepool中呢?
因为alloc、new、copy、allocWith...、copyWith...等关键字创建的对象是不会添加到autoreleasepool中的。
那么如果添加如下代码:
NSString *str = [NSString stringWithFormat:@"%@",@"123"];
会添加到autoreleasepool吗?答应是:不会。
为什么呢?因为这个对象是tagged point对象。
如果改成如下代码:
NSString *str = [NSString stringWithFormat:@"%@",@"1234567890"];
答应是:会添加到autoreleasepool。因为已经不是tagged point对象,而且一个nsstring对象了。超过9个字符就无法保存在地址中了。

我们在来看一段代码:
for (int i = 0; i < 100000000; i ++) {
NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
}
来看运行:
发现执行了100000000次创建NSString对象,cpu和内存快速上涨到1G。
接下来,我们来修改代码:
for (int i = 0; i < 100000000; i ++) {
@autoreleasepool {
NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
}
}
我们发现,只有cpu上涨了,内存保持在8.9MB没有太大的变化。
为什么加了AutoreleasePool的代码可以保证内存不大涨呢?AutoreleasePool底层做了什么呢?我们接着探索。
小结:
AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出起作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将会延迟执行。
二、AutoreleasePool的数据结构
我们继续探索:
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
clang main.m 生成 main.cpp
打开main.cpp查看源码:
搜索__AtAutoreleasePool源码:
我们看到代码:
atautoreleasepoolobj = objc_autoreleasePoolPush();//构造函数
objc_autoreleasePoolPop(atautoreleasepoolobj);//析构函数
搜索objc_autoreleasePoolPush进入源码:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
根据AutoreleasePoolPage::push(); 来看下AutoreleasePoolPage的数据结构
//AutoreleasePoolPage 结构继承自AutoreleasePoolPageData
class AutoreleasePoolPage 结构继承自 : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
#endif
//........................省略...........................//
}
发现AutoreleasePoolPage 结构继承自AutoreleasePoolPageData 数据结构:
struct AutoreleasePoolPageData
{
//........................省略...........................//
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
//........................省略...........................//
};
magic:用来校验AutoreleasePoolPage的结构是否完整;next:指向最新添加的autoreleased对象的下一个位置,初始化时指向begin();thread:指向当前线程;parent:指向父结点,第一个结点的parent值为nil;child:指向子结点,最后一个结点的child值为nil;depth:代表深度,从0开始,往后递增1;hiwat:代表 high water mark 最大入栈数量标记;
三、AutoreleasePool的添加数据
我们先来看下AutoreleasePoolPage 的注释:
/***********************************************************************
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
//线程的自动释放池是一个指针堆栈。(说明自动释放池肯定和线程是有关联的)
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
//每个指针要么是将要释放的对象,要么是一个POOL_BOUNDARY,作为自动释放池的边界。
(自动释放池里面的数据有两种类型,一种是将要被释放的对象,另一种是POOL_BOUNDARY,
超出POOL_BOUNDARY,就相当于不在当前释放池的范围之内了。)
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
//有一个指向该池的POOL_BOUNDARY的指针。当池被弹出时,每个比哨兵更热的对象
都会被释放。(在调用pop函数的时候,整个自动释放池里面的对象都会被释放。)
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
//栈被分成一个双向链接的页面列表。根据需要添加和删除页面。(自动释放池是一个双向列表
的栈结构。它的每个节点都是AutoreleasePoolPage。)
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
//线程的本地存储指向热页,其中存储了新的自动释放对象。(
自动释放池里面的数据也有线程缓存)
**********************************************************************/
我们在进入AutoreleasePoolPage::push();查看push函数。
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {//判断是否初始化
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);//初始化 newpage
} else {
dest = autoreleaseFast(POOL_BOUNDARY);//加入哨兵对象
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
查看autoreleaseNewPage(POOL_BOUNDARY)源码:
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage();//线程的快速缓存中找到AutoreleasePoolPage 没有返回nil
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);//nil 就执行创建AutoreleasePoolPage第一页并添加添加哨兵对象
}
这个hotpage函数做了什么?得进入源码查看:
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);//线程的快速缓存中找到AutoreleasePoolPage
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;//没找到返回nil
if (result) result->fastcheck();
return result;
}
大概意思是:线程的快速缓存中找到AutoreleasePoolPage则返回nil,如果有值就返回找到的AutoreleasePoolPage。
先看没有找到AutoreleasePoolPage的情况:autoreleaseNoPage(obj)做了什么?
主要是说,创建一个AutoreleasePoolPage并设置它为热页,把传入的obj(哨兵对象)添加到这页中(第一页)。
在看下new AutoreleasePoolPage(nil);具体做了什么?
看下begin()做了什么?
//this=AutoreleasePoolPage+(page大小56)等于内存平移,的地址
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
然后我们接着看下autoreleaseFullPage(obj, page); 线程的快速缓存中找到AutoreleasePoolPage的情况。
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;//满了,找子节点
else page = new AutoreleasePoolPage(page);//子节点也满了,创建一个AutoreleasePoolPage
} while (page->full());//判断是否满了
setHotPage(page);//设置成热页
return page->add(obj);
}
找到这个page,如果满了->找子节点。如果子节点也满了,创建一个AutoreleasePoolPage页。 设置page为热页,并添加obj到page中。
双向链表示意图:

四、Autorelease对象的释放
先来看下autorelease和runloop的关系图:

- App启动后,苹果在主线程RunLoop里注册了两个Observer其回调都是
_wrapRunLoopWithAutoreleasePoolHandler()。 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。其order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。第二个Observer监视了两个事件:BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。这个Observer的order是2147483647,优先级最低,保证其释放池发生在其他所有回调之后。
⼦线程的autorelease对象
子线程中一旦出现了Autorelease对象,就会创建自动释放池。子线程中创建自动释放池的方式和主线程是一样的,也是调用autoreleaseNoPage。也就是说在子线程中的Autorelease对象,咱们同样不需要进行手动的内存管理,也不会内存泄漏。
AutoreleasePool的释放数据
我们在源码中看到:
AutoreleasePoolPage 的大小在4KB 4096。 所有一页page可以存 4096-56-8(哨兵)= 4032/8 = 504 个对象。
我们在通过一个例子验证,上述理论:
@autoreleasepool {
for(int i=0;i<1010;i++){
__autoreleasing NYPerson *p = [NYPerson alloc];
}
_objc_autoreleasePoolPrint();//查看自动释放池
}
查看打印:
第一页为full cold页存504个对象,哨兵对象占用一个。
第二页为full页存505个对象。
第三页为hot页存了一个对象。
所以应该autoreleasepoolpage第一页可以存504个对象,其他页可以存505个对象。
我们接着看objc_autoreleasePoolPop(atautoreleasepoolobj)进入到源码:
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
在进入pop源代码:
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {//判断边界
// Popping the top-level placeholder pool.
page = hotPage();//获取热页
if (!page) {
// Pool was never used. Clear the placeholder.
return setHotPage(nil);//设置热页为nil
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
page = coldPage();//获取cold页
token = page->begin();//把cold页第一个设置为哨兵
} else {
page = pageForPointer(token);//token就是哨兵,获取哨兵所在页
}
stop = (id *)token;//哨兵
if (*stop != POOL_BOUNDARY) {//是否是哨兵
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);//报异常
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
return popPage<false>(token, page, stop);//释放链表中的对象到stop位置。
}
先看下pageForPointer做了什么:
在接着看popPage<false>(token, page, stop);源码:
进行跟踪page->releaseUntil的源码:
void releaseUntil(id *stop)
{
while (this->next != stop) {//当前页的下一页不是哨兵页
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();//获取到热页
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {//循环获取热页
page = page->parent;
setHotPage(page);
}
page->unprotect();
//........................省略...........................//
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {//判断不是哨兵
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
// release count+1 times since it is count of the additional
// autoreleases beyond the first one
for (int i = 0; i < count + 1; i++) {
objc_release(obj);
}
#else
objc_release(obj);//将这段代码插入释放池中的每对象
#endif
}
}
setHotPage(this);//设置当前页为热页
//........................省略...........................//
}
小结:
objc_autoreleasePoolPop函数通过token(哨兵对象)所在的page,如何根据hotpage通过releaseUntil函数从hotpage页一直插入释放代码objc_release(obj)直到token(哨兵对象)为止。
在ARC环境下,autorelease 对象在什么时候释放?
- 如果是手动添加到@autoreleasepool里面的
autorelease对象,是在出了@autoreleasepool的作用域之后被释放。 - 如果是系统添加到自动释放池的
autorelease对象的释放时机就是由RunLoop控制的,会在每一次的RunLoop循环结束时释放。 - 如果是子线程没有开启RunLoop,就是在子线程销毁的时候释放。
总结
AutoreleasePool初探:AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出起作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将会延迟执行。AutoreleasePool的数据结构:发现AutoreleasePoolPage 结构继承AutoreleasePoolPageData 数据结构,56个字节是一个双向链表结构。AutoreleasePool的添加数据:通过objc_autoreleasePoolPush函数压栈,第一页第一个会创建一个哨兵对象也就是autoreleaseNoPage(obj)函数,然后autoreleaseFullPage(obj, page);线程的快速缓存中找到热页添加对象到热页中。Autorelease对象的释放:objc_autoreleasePoolPop函数通过token(哨兵对象)所在的page,如何根据hotpage通过releaseUntil函数从hotpage页一直插入释放代码objc_release(obj)直到token(哨兵对象)为止。
给朋友打个广告: