OC

iOS中的autorelease和autoreleasepool

Posted by sunzhongliang on September 6, 2019

autorelease

MRC时代,我们如果希望一个对象延迟释放的时候,通常会把这个对象标记为autorelease, 如

NSString *str = [[[NSString alloc] initWithString:@"hello"] autorelease];

后来在ARC的时候,我们甚至不用关心一个对象是什么时候release的, 系统总是能够在合适的时候帮我们去释放这个对象, 背后它究竟做了什么?

ARC和MRC下的autorelease

ARC

ARC是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码(release)以及在 Runtime做一些优化。

ARC的情况下,编译器会在RunLoop休眠前执行释放的,而它能够释放的原因就是系统在每个runloop迭代中都加入了自动释放池PushPop

MRC

MRC机制下,对一个对象标记autorelease后,这个对象并不会马上被释放,而是当这段语句所处的 autoreleasepool 进行 drain 操作时,所有标记了 autorelease 的对象的 retainCount 会被 -1。即 release 消息的发送被延迟到 pool 释放的时候了。

autoreleasepool

ARC下,我们使用@autoreleasepool{}来使用一个autoreleasepool,随后编译器将其改写成下面的样子:

void *context = objc_autoreleasePoolPush();
// @autoreleasepool{}中的代码
objc_autoreleasePoolPop(context);

autoreleasePoolPage的结构

class AutoreleasePoolPage 
{
    PAGE_MAX_SIZE//最大size 4096字节
    magic_t const magic; //用来校验AutoreleasePoolPage的结构是否完整
    id *next;//指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满
    pthread_t const thread;//指向当前线程,一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage;
    AutoreleasePoolPage * const parent;//指向父结点,第一个结点的 parent 值为 nil;
    AutoreleasePoolPage *child;//指向子结点,最后一个结点的 child 值为 nil;
    uint32_t const depth;//代表深度,第一个page的depth为0,往后每递增一个page,depth会加1;
}

autoreleasePool工作原理

autoreleasepool本质上就是一个指针堆栈,内部结构是由若干个以AutoreleasePoolPage对象为结点的双向链表组成,系统会在需要的时候动态地增加或删除page节点,如下图即为AutoreleasePoolPage组成的双向链表:

运行流程:

  1. 在运行循环开始前,系统会自动创建一个autoreleasepool(一个autoreleasepool会存在多个AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush函数,runtime会向当前的AutoreleasePoolPage中添加一个POOL_BOUNDARY(哨兵对象),代表autoreleasepool的起始边界地址),并返回此哨兵对象的内存地址
  2. next指针则会指向POOL_BOUNDARY(哨兵对象)后面的地址(对象地址1)
  3. 后面我们创建对象,如果对象调用了autorelease方法(ARC编译器会给对象自动插入autorelease),则会被添加进AutoreleasePoolPage中,位置是在next指针指向的位置,如上面next指向的是对象地址1,这是后添加的对象地址就在对象地址1这里,然后next就会 指向到对象地址2 ,以此类推,每添加一个地址就会向前移动一次,直到指向end()表示已存满
  4. 当不断的创建对象时,AutoreleasePoolPage不断存储对象地址,直到存满后,则又会创建一个新的AutoreleasePoolPage,使用child指针和parent指针指向下一个page上一个page,从而形成一个双向链表.
  5. 当调用objc_autoreleasePoolPop(哨兵对象地址)时,假设我们如上图,添加最后一个对象地址8,那么这时候就会依次由对象地址8 -> 对象地址1,每个对象都会调用release方法释放,直到遇到哨兵对象地址为止

autoreleasepool的嵌套

当多个autoreleasepool嵌套,对象的释放,会是什么情况呢? 每次新建一个@autoreleasepool,就会执行一次push操作,对应的具体实现就是往AutoreleasePoolPage中的next位置插入一个POOL_BOUNDARY(哨兵对象)

@autoreleasepool   {//autoreleasepool1
    NSObject * obj1 = [[NSObject alloc] init];
   
    @autoreleasepool  {//autoreleasepool2
        NSObject * obj2 = [[NSObject alloc] init];
        NSObject * obj3 = [[NSObject alloc] init];
    }
}

释放流程:

  1. 当autoreleasepool1创建时,会添加哨兵对象1,接着obj1的创建,则把obj1地址添加进来。
  2. 当autoreleasepool2创建,会添加哨兵对象2,位置是obj1后面(上面next指针指向原理),然后依次把obj2和obj3加进来。
  3. 当autoreleasepool2结束时,obj3,obj2,会找到离它们最近的autoreleasepool即 autoreleasepool2,然后依次调用release,直到哨兵对象2位置。
  4. 当autoreleasepool1结束时,当obj1调用release,直到哨兵对象1位置,

ARC与MRC下autoreleasepool的区别

MRC下需要手动管理自动释放池的创建和释放,ARC下只需要使用@autoreleasepool将对应的代码包含起来即可。
MRC下调用自动释放池release方法后,会对autorelease对象进行释放,因此,此后访问的属性变量为野指针,再去访问自然会导致crash。而ARC下,@autoreleasepool并不会立即在结束括号符后,立即释放person变量,而是会在runloop休眠前进行释放

autoreleasepool的用处

for (int i = 0; i < 100000000; i++)
{
    @autoreleasepool
    {
        NSString* string = @"ab c";
        NSArray* array = [string componentsSeparatedByString:string];
    }
}

当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool 会在整个 runloop circle 结束之后才进行 drain,手动创建的话,会在 block 结束之后就进行 drain 操作。 如果不使用 autoreleasepool ,需要在循环结束之后释放 100000000 个字符串,如果 使用的话,则会在每次循环结束的时候都进行 release 操作。

Runloop和Autorelease

iOS在主线程的Runloop中注册了2个Observer 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() 第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush() 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

main.m 中 Autorelease Pool

Xcode 11前

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

Xcode11之前,是将整个应用程序运行放在@autoreleasepool内的,由于主线程Runloop的存在,这个函数永远不会返回,意味着程序结束后main函数中的autorelease对象才会释放。

Xcode 11后

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

Xcode 11后,触发主线程 RunLoopUIApplicationMain 函数放在了 @autoreleasepool 外面,这可以保证 @autoreleasepool 中的 autorelease 对象在程序启动后立即释放。正如新版本的 @autoreleasepool 中的注释所写 “Setup code that might create autoreleased objects goes here.”,这里的autoreleasepool中的autorelease对象在程序启动后立即释放

本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.