Autoreleasepool

96
thinkq
1.7 2018.05.27 02:00* 字数 2507

局部释放池

创建一个新的自动释放池的方法:
ARC下:

@autoreleasepool {
  Student *s = [[Student alloc] init];
}

这相当于MRC下:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init;
Student *s = [[Student alloc] init];
[pool drain];

其中对象s会被加入到自动释放池,当ARC下代码执行到右大括号时(相当于MRC执行代码[pool drain];)会对池中所有对象依次执行一次release操作

等等,在ARC下我直接这样写:

{
  Student *s = [[Student alloc] init];
}

MRC下我直接这样写:

Student *s = [[Student alloc] init];
[s release];

效果和你用自动释放池是一样的啊,那我为什么要用这破玩意啊,还要初始化对象浪费时间浪费内存。

以上用到的Autoreleasepool叫做局部释放池,在特定场景下有其用处,后面会详细讲解。

Autoreleasepool使用场景

还记得MRC下这么一种情况吗:

@implementation Student
+ (instancetype)student {
    Student *student = [[Student alloc] init];
    return student;
}
@end

分析一下:student创建并持有对象,返回值对象引用计数为1
然后调用的地方:

Student *s = [Student student];

s一通快乐的使用,唯独最后忘记调用[s release];,然后对象引用计数始终是1内存泄漏,因为调用者认为我都没有对对象做retain操作,我不持有对象,你为什么让我释放对象,这不符合内存管理规则,崩溃了你负责啊?!不明白?请默念内存管理规则:

  1. 自己生成的对象自己持有
  2. 非自己生成的对象,自己也能持有
  3. 不再需要自己持有的对象时必须释放
  4. 自己不持有的对象无法释放

那要不这样写吧:

@implementation Student
+ (instancetype)student {
    Student *student = [[Student alloc] init];
    [student release];
    return student;
}
@end

分析一下:student创建并持有对象,对象引用计数为1,student释放对象,对象引用计数为0,对象释放,返回值对象为...哪他妈还有返回值对象啊对象被释放了

既然不能自己释放对象,也不能要求调用方用完对象之后释放,那该怎么办呢?这时候就可以看到使用Autoreleasepool的正确姿势了:

@implementation Student
+ (instancetype)student {
    Student *student = [[Student alloc] init];
    [student autorelease];
    return student;
}
@end

调用方:

    Student *s = [Student student];
    [s retain];
    // work
    [s release];

通过调用autorelease方法把释放对象的任务交给Autoreleasepool对象,当Autoreleasepool对象执行到[pool drain]方法的时候会对自动释放池中所有的对象执行一次release操作,然后+ (instancetype)student方法中创建的对象引用计数就会被-1了,如果引用计数变为0了,对象自然就释放了
调用方获取对象以后自己retain持有一下对象,防止使用期间对象被释放了,用完release释放一下

以上只是在MRC下自动释放的一种使用情形,在ARC时这种情形由编译器自动为我们加上__autorealeasing修饰符id __autorealeasing obj = [NSMutableArray array];,作用和MRC下调用autorelease方法一样,ARC 还有什么对象由 Autoreleasepool 管理呢,可以参看:引用计数带来的一次讨论

RunLoop释放池

看似很完美了,但是有没有想过我们直接调用autorelease方法就可以把释放对象的任务交给Autoreleasepool对象,Autoreleasepool对象从哪里来?Autoreleasepool对象又会在何时调用[pool drain]方法?

要解答以上两个问题,首先要了解NSRunLoop,可以看下ibireme深入理解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,优先级最低,保证其释放池子发生在其他所有回调之后。

对于每一个Runloop运行循环,系统会隐式创建一个Autoreleasepool对象,+ (instancetype)student中执行autorelease的操作就会将student对象添加到这个系统隐式创建的
Autoreleasepool对象中——这回答了Autoreleasepool对象从哪里来

当Runloop执行完一系列动作没有更多事情要它做时,它会进入休眠状态,避免一直占用大量系统资源,或者Runloop要退出时会触发执行_objc_autoreleasePoolPop()方法相当于让Autoreleasepool对象执行一次drain方法,Autoreleasepool对象会对自动释放池中所有的对象依次执行依次release操作——这回答了Autoreleasepool对象又会在何时调用[pool drain]方法

子线程上的Autorelease

子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?

感觉比较难以回答,需要很细致的读过 Runtime 、Autoreleasepool 的源码才可以。我也是参考了 StackOverFlow 的回答:does NSThread create autoreleasepool automaticly now?。我再来简单阐述下,在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage,如果你还是不理解,可以先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用 page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!StackOverFlow 的作者也说道,这个是 OS X 10.9+和 iOS 7+ 才加入的特性。并且苹果没有对应的官方文档阐述此事,但是你可以通过源码了解。

嗯,这里是照抄的,只是为了给自己做个笔记
作者:Joy___
链接:https://www.jianshu.com/p/f87f40592023
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

看个例子

要点一: 当AutoreleasePool出现嵌套的时候,在内层调用autorelease方法,会把对象添加到内层的AutoreleasePool对象生成的自动释放池里。具体实现可以看黑幕背后的Autorelease
要点二: 由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的。同样了解自黑幕背后的Autorelease
要点三:将对象放入自动释放池不会引起引用计数+1

在查资料是看到AutoreleasePool详解和runloop的关系中有如下实验代码:

@interface ViewController ()
@property (nonatomic, weak) NSString *string_weak;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // 场景 1
    NSString *string = [NSString stringWithFormat:@"1234567890"];
    self.string_weak = string;
    NSLog(@"string: %@",self.string_weak);
    
    //场景 2
//    @autoreleasepool {
//        NSString *string = [NSString stringWithFormat:@"1234567890"];
//        _string_weak = string;
//    }
//    NSLog(@"string: %@",_string_weak);
    
    // 场景 3
//    NSString *string = nil;
//    @autoreleasepool {
//        string = [NSString stringWithFormat:@"1234567890"];
//        _string_weak = string;
//    }
//    NSLog(@"string: %@",self.string_weak);
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"string: %@", self.string_weak);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"string: %@", self.string_weak);
}

@end

执行后打印结果为

// 场景 1
2018-05-27 00:58:17.791343+0800 原子操作[14045:1419769] string: 1234567890
2018-05-27 00:58:17.791570+0800 原子操作[14045:1419769] string: 1234567890
2018-05-27 00:58:17.794914+0800 原子操作[14045:1419769] string: (null)

// 场景 2
2018-05-27 00:59:00.764576+0800 原子操作[14063:1420728] string: (null)
2018-05-27 00:59:00.764798+0800 原子操作[14063:1420728] string: (null)
2018-05-27 00:59:00.767894+0800 原子操作[14063:1420728] string: (null)

// 场景 3
2018-05-27 00:59:30.974246+0800 原子操作[14079:1421468] string: 1234567890
2018-05-27 00:59:30.974407+0800 原子操作[14079:1421468] string: (null)
2018-05-27 00:59:30.977082+0800 原子操作[14079:1421468] string: (null)

原博是按照对象加入自动释放池会让对象引用计数+1解释,并且解释通了,但是请注意以下代码与打印(MRC下):

+ (instancetype)student {
    Student *student = [[Student alloc] init];
    NSLog(@"创建后引用计数:%lu",(unsigned long)[student retainCount]);
    [student autorelease];
    NSLog(@"加入自动释放池后引用计数:%lu",(unsigned long)[student retainCount]);
    return student;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    Student *s = [Student student];
    NSLog(@"调用方获取到对象时引用计数:%lu",(unsigned long)[s retainCount]);
}

打印结果:

2018-05-27 01:05:32.839710+0800 MRC[14144:1427133] 创建后引用计数:1
2018-05-27 01:05:32.839825+0800 MRC[14144:1427133] 加入自动释放池后引用计数:1
2018-05-27 01:05:32.839929+0800 MRC[14144:1427133] 调用方获取到对象时引用计数:1

对象引用计数始终为1,加入自动释放池不会让引用计数+1,我开始是按照博客所说对象加入自动释放池会让对象引用计数+1的说法理解的,但是当看到这段代码的时候产生了疑惑:

@implementation Student
+ (instancetype)student {
    Student *student = [[Student alloc] init];
    [student autorelease];
    return student;
}
@end

将对象放入自动释放池是为了等下自动释放池能让对象执行一次release操作自动释放对象,让对象的引用计数在这个方法体内达到收支平衡,避免内存泄漏。Student *student = [[Student alloc] init];后对象引用计数为1,[student autorelease];后对象引用计数为2,当Runloop即将进入休眠时,释放自动释放池,student对象会被执行一次release操作,引用计数变为1,还是释放不了!所以将对象放入自动释放池不会引起引用计数+1

结合要点一二三,自己尝试解释场景1,2,3吧

不过以上实验代码说明了我们可以手动干预Autorelease对象的释放时机,利用这个我们就可以利用局部释放池做些事情了

局部释放池的应用

看下边这段代码:

for (int i = 0; i < largeNumber; i++) { 
    NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
    str = [str stringByAppendingString:@" - world"]; 
}

要点:

  • [NSString stringWithFormat:@"hello -%04d", i]方法创建的对象会加入到自动释放池里,对象的释放权交给了RunLoop 的释放池
  • RunLoop 的释放池会等待Runloop即将进入睡眠或者即将退出的时候释放一次
  • for循环中线程一直在做事情,Runloop不会进入睡眠

综上:
上边的代码for循环生成的NSString对象会无法及时释放,造成瞬时内存占用过大

解决办法,每次循环时都手动创建一个局部释放池,及时创建,及时释放,这样NSString对象就会及时得到释放

    for (int i = 0; i < largeNumber; i++) {
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
            str = [str stringByAppendingString:@" - world"];
        }
    }

在for循环大量使用imageNamed:之类的方法生成UIImage对象可能是个更要命的事情,内存随时可能因为占用过多被系统杀掉。
这种情况下利用Autoreleasepool可以大幅度降低程序的内存占用。偷一张土土哥博客的图来说明:

image.png

黑幕背后的Autorelease

我这里只是了解了Autorelease做的事情,具体到代码级别是怎么做的,以及Autorelease是怎样的数据结构可以看这里:黑幕背后的Autorelease

iOS 内存管理 —— MRC & ARC
各个线程 Autorelease 对象的内存管理
NSRunloop,runloop,autoReleasePool和thread的关系理解
RunLoop和autorelease的一道面试题
@autoreleasepool-内存的分配与释放

日记本