iOS关于自动释放池

本文是经过翻阅博客、论坛学习以及代码调试的总结,如有疑惑或不准确的地方,欢迎评论沟通指正。

通读本文你将理解:

  1. 自动释放池底层结构
  2. 自动释放池何时释放(换言之autorelease何时执行release操作)
  3. ARC下什么样的初始化方法系统会为我们做一次autorelease操作

Objc源码下载地址https://opensource.apple.com/tarballs/objc4/

一、自动释放池底层结构

总结一句话就是:以栈为节点,以双向链表形式组合而成的一个数据结构。通俗一些讲自动释放池是以多个AutoreleasePoolPage为结点,通过链表的方式串连起来的结构,这一整串就是自动释放池。


iOS关于自动释放池_第1张图片
双向链表结构

iOS关于自动释放池_第2张图片
结点对象
  1. magic 用于对当前 AutoreleasePoolPage 完整性的校验
  2. thread 保存了当前页所在的线程
  3. id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  4. 每个AutoreleasePoolPage大小均为4096


    iOS关于自动释放池_第3张图片
    释放池结构

二、自动释放池何时释放

//ARC 环境
- (void)viewDidLoad {
    [super viewDidLoad];
   @autoreleasepool {
        YMObject *object = [[YMObject alloc] init];
    };
}

实际的函数调用是这样的

iOS关于自动释放池_第4张图片
汇编调用

iOS关于自动释放池_第5张图片
Pasted Graphic 2.jpg

需要注意的是,整个程序中push和pop的操作都是一一对应的,下面会说明
来分析一下 objc_autoreleasePoolPush和objc_autoreleasePoolPop这两个函数做了什么

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}


void *
_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void
_objc_autoreleasePoolPop(void *ctxt)
{
    objc_autoreleasePoolPop(ctxt);
}

Objc4源码中可以看到实际上都是AutoreleasePoolPage类的push和pop函数。Push函数中会调用一个比较核心的方法: autoreleaseFast

    //这个函数的作用就是,找到最顶层的一个AutoreleasePoolPage对象,如果没有那就创建一个;如果找到了,判断他是否已经装满了full(),因为一个AutoreleasePoolPage只有4096个字节大小,如果满了那就会调用autoreleaseNoPage()创建一个AutoreleasePoolPage对象并添加add;如果没满则直接执行add(obj)。
    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();  //hotPage()函数会对应线程去取自动释放池,这里也可以看出释放池和线程是一一对应的关系
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }
需要注意的是上述add(obj)中的obj是一个POOL_BOUNDARY对象(可以称为哨兵对象),并不是我们的autorelease的对象,每次执行push操作时都会插入一个哨兵对象,并且把哨兵对象的地址作为返回值返回了,pop函数需要用到这个哨兵对象的地址,对应的每次pop都是寻找到上一个哨兵对象,对期间所有的autorelease对象执行一次release操作。

再来看下pop函数:其中会调用一个比较核心的函数releaseUntil(id *stop)

void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        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中的autorelease对象已释放完毕则会重新遍历父结点的page,知道找到传递来的哨兵对象为止
                page = page->parent;
                setHotPage(page);
            }
            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }
        setHotPage(this);
#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

上述就是我们自己通过@autoreleasePool{ }形式创建的释放池的创建和销毁,以及其内部的主要逻辑分析。
当然系统默认也是会在当前runloop结束本次循环即将进入休眠的时候也会执行一次pop操作来对释放池内的对象进行一次release操作,然后再重新执行一次push操作。

再补充一下:(直接从别处搬来的…)

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  1. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  2. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
总结可以下来就是:app启动 调用了一次push来创建释放池,中间我们自己可能会通过@autoreleasePool{ }再执行n次push和pop操作,runloop即将休眠时又会调用一次pop和一次push,runloop在Exit时,又会调用一次pop操作,可以说是一个push就有一个pop与之对应的,从而保证对象能正常释放。

三、ARC下什么样的初始化方法系统会为我们做一次autorelease操作

ARC下以alloc/new/copy/mutableCopy开头的方法,其返回的对象归调用者所有。归调用者所有的意思是:调用上述四个方法的那段代码负责释放方法返回的对象。(摘自Effective Objective-C 2.0 P124)

非alloc/new/copy/mutableCopy开头的方法编译器都会自动帮我们调用autorelease方法。

//自定义类方法
+ (instancetype)createSark {
    return [self new]; //这里是有autorelease的(谁创建谁释放)
}
// caller
Sark *sark = [Sark createSark]; //此处有retain操作,作用域结束会有release操作

值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略:

秉着谁创建谁释放的原则,返回值需要是一个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的形式进行读写.
在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。这样就省去了两个操作:autorelease和外部的一次retain操作,对于性能提高很多。

自动释放池用途

for (int i = 0; i < 100000000; i++)
{
    @autoreleasepool
    {
        NSString* string = @"ab c";
        NSArray* array = [string componentsSeparatedByString:string];   //放入自动释放池中,从而避免大量局部变量要等待runloop将要休眠才释放从而造成的内存峰值。
    }
}

参考文章:
https://www.jianshu.com/p/8b011b844231
https://blog.sunnyxx.com/2014/10/15/behind-autorelease/

你可能感兴趣的:(iOS关于自动释放池)