局部释放池
创建一个新的自动释放池的方法:
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操作,我不持有对象,你为什么让我释放对象,这不符合内存管理规则,崩溃了你负责啊?!不明白?请默念内存管理规则:
- 自己生成的对象自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有的对象时必须释放
- 自己不持有的对象无法释放
那要不这样写吧:
@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可以大幅度降低程序的内存占用。偷一张土土哥博客的图来说明:
黑幕背后的Autorelease
我这里只是了解了Autorelease做的事情,具体到代码级别是怎么做的,以及Autorelease是怎样的数据结构可以看这里:黑幕背后的Autorelease
iOS 内存管理 —— MRC & ARC
各个线程 Autorelease 对象的内存管理
NSRunloop,runloop,autoReleasePool和thread的关系理解
RunLoop和autorelease的一道面试题
@autoreleasepool-内存的分配与释放