iOS内存管理之AutoreleasePool

7,184 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

一、AutoreleasePool初探

我们先看一个例子代码:

@autoreleasepool {
        // insert code here...
        NSLog(@"Hello world!");
        NYPerson *p = [NYPerson alloc];
        _objc_autoreleasePoolPrint();//查看自动释放池
    }

运行查看打印结果: image.png 在打印中没有看到NYPerson的内存信息。

如果我们加上 __autoreleasing NYPerson *p = [NYPerson alloc]; 在看下打印: image.png 现在我们看到了NYPerson的内存信息,NYPerson对象被添加到自动释放池中。

为什么不添加__autoreleasing关键字,不会添加到autoreleasepool中呢?

因为alloc、new、copy、allocWith...、copyWith...等关键字创建的对象是不会添加到autoreleasepool中的。

那么如果添加如下代码:

NSString *str = [NSString stringWithFormat:@"%@",@"123"];

会添加到autoreleasepool吗?答应是:不会。 image.png 为什么呢?因为这个对象是tagged point对象。

如果改成如下代码:

NSString *str = [NSString stringWithFormat:@"%@",@"1234567890"];

答应是:会添加到autoreleasepool。因为已经不是tagged point对象,而且一个nsstring对象了。超过9个字符就无法保存在地址中了。 image.png

我们在来看一段代码:

    for (int i = 0; i < 100000000; i ++) {
            NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
    }

来看运行: image.png 发现执行了100000000次创建NSString对象,cpu和内存快速上涨到1G。

接下来,我们来修改代码:

    for (int i = 0; i < 100000000; i ++) {
        @autoreleasepool {
            NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
        }
    }

我们发现,只有cpu上涨了,内存保持在8.9MB没有太大的变化。 image.png 为什么加了AutoreleasePool的代码可以保证内存不大涨呢?AutoreleasePool底层做了什么呢?我们接着探索。

小结

AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出起作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将会延迟执行

二、AutoreleasePool的数据结构

我们继续探索:

@autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }

clang main.m 生成 main.cpp image.png 打开main.cpp查看源码: image.png 搜索__AtAutoreleasePool源码: image.png 我们看到代码:

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)做了什么? image.png 主要是说,创建一个AutoreleasePoolPage并设置它为热页,把传入的obj(哨兵对象)添加到这页中(第一页)。

在看下new AutoreleasePoolPage(nil);具体做了什么? image.png 看下begin()做了什么?

    //this=AutoreleasePoolPage+(page大小56)等于内存平移,的地址
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

image.png 然后我们接着看下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中。

双向链表示意图: image.png

四、Autorelease对象的释放

先来看下autorelease和runloop的关系图: image.png

  • 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的释放数据

我们在源码中看到: image.png 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();//查看自动释放池
    }

查看打印: image.png 第一页为full cold页存504个对象,哨兵对象占用一个。 image.png 第二页为full页存505个对象。 image.png 第三页为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做了什么: image.png 在接着看popPage<false>(token, page, stop);源码: image.png 进行跟踪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 对象在什么时候释放?

  1. 如果是手动添加到@autoreleasepool里面的autorelease对象,是在出了@autoreleasepool的作用域之后被释放。
  2. 如果是系统添加到自动释放池的autorelease对象的释放时机就是由RunLoop控制的,会在每一次的RunLoop循环结束时释放。
  3. 如果是子线程没有开启RunLoop,就是在子线程销毁的时候释放。

总结

  1. AutoreleasePool初探AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出起作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将会延迟执行
  2. AutoreleasePool的数据结构:发现AutoreleasePoolPage 结构继承AutoreleasePoolPageData 数据结构,56个字节是一个双向链表结构。
  3. AutoreleasePool的添加数据:通过objc_autoreleasePoolPush函数压栈,第一页第一个会创建一个哨兵对象也就是autoreleaseNoPage(obj)函数,然后autoreleaseFullPage(obj, page); 线程的快速缓存中找到热页添加对象到热页中。
  4. Autorelease对象的释放objc_autoreleasePoolPop函数通过token(哨兵对象)所在的page,如何根据hotpage通过releaseUntil函数从hotpage页一直插入释放代码objc_release(obj)直到token(哨兵对象)为止。

给朋友打个广告:

WechatIMG7_副本.jpg