Autoreleasepool

局部释放池

创建一个新的自动释放池的方法:
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___
链接://www.greatytc.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-内存的分配与释放

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 1、[NSObject alloc]在创建完对象后,会让该对象的retainCount+1,后续的init为初始化...
    naiyi阅读 1,513评论 0 4
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,305评论 8 265
  • 1.趣味思考 ARC机制下对象指针有三种修饰符,分别是__ strong、__ autoreleasing、__ ...
    Emiya_zhang阅读 2,235评论 8 3
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,249评论 0 11
  • 所有的风 只有一个方向 所有的灯 只为一部影像 我 在风中沉睡 在灯下迷醉 醒来时 秋风无处凉, 落叶梦一场。 原...
    北觉阅读 193评论 0 0