首先我们先看个好玩的事情~
#import "ViewController2.h"
@interface ViewController2 () {
__weak id tracePtr;
}
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
tracePtr = str;
}
- (void)viewWillAppear:(BOOL)animated {
NSLog(@"viewWillAppear tracePtr: %@", tracePtr);
}
- (void)viewDidAppear:(BOOL)animated {
NSLog(@"viewDidAppear tracePtr: %@", tracePtr);
}
@end
看到上面的代码,猜测一下输出会是什么呢?我最开始的想法应该都是null,因为tracePtr是弱指针,str在viewDidLoad结束以后就没有引用计数了,应该被回收掉,所以在viewWillAppear和viewDidAppear中再打印的时候应该就空啦。
但是实际上打印了什么嘞?
Example1[10896:167819] viewWillAppear tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[10896:167819] viewDidAppear tracePtr: (null)
是不是灰常神奇,在viewWillAppear的时候str的内存仍旧没有被清空。这是为什么呢?
autorelease
上面的问题一会儿再解决,我们先了解一下autorelease相关的方法哈。在MRC时代我们需要自己手动管理内存,当对象不用了以后,需要调用[obj release]来释放内存,但有的时候我们不希望它马上释放,需要它等一会儿在释放,例如作为函数返回值:
- (Person *)createPerson {
return [[[Person alloc] init] autorelease];
}
如果不加autorelease,直接返回一个新的person,那么由于alloc init会加一次引用计数,无论怎么也无法抵消,除非alloc后调用release或者让外部release两次,但依赖调用者release是很不容错的;而如果马上release,外部调用这个方法的拿到的就是nil了,所以这里用autorelease。
autorelease会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作。也就是调用后不是马上计数-1,而是在自动释放池销毁时再-1。
这样的话当外部createPerson以后是可以获取到一个person的,如果使用了另外的引用指向person,person的引用数暂时为2,而自动释放池销毁时,会对person执行一次release,它的计数就变为了1,由于仍旧有引用就不会被销毁;如果外部没有建新的引用,那么在自动释放池销毁时就会销毁这个对象啦。
这里的自动释放池其实是和runloop有关的,是系统自动创建维护的,每次runloop休眠的时候进行清空,后面的autoreleasepool中会解释。
我们来看下源码~
//autorelease方法
- (id)autorelease {
return ((id)self)->rootAutorelease();
}
//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
//检查是否可以优化
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
//放到auto release pool中。
return rootAutorelease2();
}
// rootAutorelease2
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
再看一下AutoreleasePoolPage的autorelease:
public: static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
autorelease方法会把对象存储到AutoreleasePoolPage的链表里*next++ = obj;
。等到auto release pool被释放的时候,把链表内存储的对象删除。所以,AutoreleasePoolPage就是自动释放池的内部实现。
autorelease释放时机
ARC时代我们是不用自己做对象释放的处理滴,但ARC其实就是对MRC包了一下,系统帮我们release和retain,ARC中也是有需要延后销毁的autorelease对象的,它们究竟在什么时候销毁的呢?
其实对象的释放是由autorelease pool来做的,而这个pool会在RunLoop进入的时候创建,在它即将进入休眠的时候对pool里面所有的对象做release操作,最后再创建一个新的pool。(RunLoop可参考:http://www.cocoachina.com/articles/11970)
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
故而,在当前RunLoop没有进入这一轮儿休眠的时候,对象是暂时不会释放的,所以如果我们不特殊处理这些autorelease变量,在他们计数为0的时候,可能也不会立刻被释放。
- 那么autorelease Pool是啥呢?
还记得MRC的[obj autorelease]
么,其实就是将obj放入了自动释放池的顶部,这个自动释放池就是autorelease Pool。
它类似一个栈,我们可以往里面push一个个新建的变量,然后在池子销毁的时候,就会把里面的变量一个个拿出来执行release方法。
@autoreleasepool与AutoreleasePool及原理
我们最经常看到的大概就是main()函数里的autoreleasepool了,如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
这个main()函数里面的池并非必需。因为块的末尾是应用程序的终止处,即便没有这个自动释放池,也会由操作系统来释放。但是这些由UIApplicationMain函数所自动释放的对象就没有池可以容纳了,系统会发出警告。因此,这里的池可以理解成最外围捕捉全部自动释放对象所用的池。
@autoreleasepool{}其实就相当于:
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
AutoreleasePoolPage是啥类?上面也出现过它的身影,那么它的定义是:
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
这里的parent和child其实就是链表的上一个和下一个,也就是说其实自动释放池AutoreleasePool里面有很多AutoreleasePoolPage,page形成一个链表结构,就像下图一样:
自动释放池AutoreleasePool是以一个个AutoreleasePoolPage组成,而AutoreleasePoolPage以双链表形成的自动释放池。
AutoreleasePoolPage中的每个对象都会开辟出虚拟内存一页的大小(也就是4096个字节),除了实例变量占据空间,其他的空间都用来存储autorelease对象的地址。
id * next指向的是栈顶对象的下一个位置,这样再放入新的对象的时候就知道放到哪个地址了,放入以后会更新next指向,让它指到新的空位。如果AutoreleasePoolPage空间被占满时,会创建一个AutoreleasePoolPage连接链表,后来的对象也会在新的page加入。
单向链表适用于节点的增加删除,双向链表适用于需要双向查找节点值的情况。这即是AutoreleasePoolPage以双链表的方式组合的原因。缺点就是空间占用较单链表大。
-
假设当前线程只有一个AutoreleasePoolPage对象,对象的内存地址如下图:
然后当一个对象发送了autorelease消息,就是将当前这个对象加入到AutoreleasePoolPage的栈顶next指向的位置。
-
每进行一次objc_autoreleasePoolPush调用时,runtime就会将当前的AutoreleasePoolPage加入一个哨兵对象,就会变成下面结构:
objc_autoreleasePoolPop的时候,根据传入的哨兵位置找到哨兵所对应的page
将晚于哨兵对象插入的autorelease对象都发送一个release消息,并移动next指针到正确的位置。
objc_autoreleasePoolPush返回值也就是哨兵对象的地址,被objc_autoreleasePoolPop作为参数。
@autoreleasepool{} 就是先push一下得到哨兵地址,然后把包裹的创建的变量一个个放入AutoreleasePoolPage,最后pop将哨兵地址之后的变量都拿出来一个个执行release。所以@autoreleasepool和AutoreleasePool不是一个含义哦!
ARC与MRC下如何创建自动释放池
NSAutoreleasePool(只能在MRC下使用)
@autoreleasepool {}代码块(ARC和MRC下均可以使用)
// MRC
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];
// ARC
@autoreleasepool {
id obj = [NSObject alloc] init];
}
@autoreleasepool应用场景
- 循环优化
如果你尝试跑下面的代码,你的内存会持续性的增加,几乎电脑容量就爆了。。毕竟100000000非常大,所以如果想尝试建议改小哈。
for (int i = 0; i < 100000000; i++) {
UIImage *image = [UIImage imageNamed:@"logo"];
}
这个内存爆的原因其实就是image作为局部变量,在不特殊处理的时候会在runLoop休眠时再被销毁,不会立即销毁。
所以如果想解决这个问题应该改为:
for (int i = 0; i < 100000000; i++) {
@autoreleasepool{
UIImage *image = [UIImage imageNamed:@"logo"];
}
}
- 如果你的应用程序或者线程是要长期运行的,或者长期在后台中运行的任务,因为任务运行中runloop是不会休眠的,如果产生大量需要autorelease的对象,需要手动@autoreleasepool,否则不会立刻释放导致内存增加
子线程中Autorelease的释放
子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。
在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都有autorelease的管理操作。
就算插入没有pop也没关系,在线程exit的时候会释放资源。
最后解答一下最开始的问题:
通常非alloc、new、copy、mutableCopy出来的对象都是autorelease的,比如[UIImage imageNamed:]、[NSString stringWithFormat]、[NSMutableArray array]等。(会加入到最近的autorelease pool哈)
也就是说 [NSString stringWithFormat:@"%@", @"ss"]方法内部类似于:
+(NSString *) stringWithFormat {
NSString *str = [[NSString alloc] initWithXXX];
return [str autorelease];
}
因为alloc init已经对引用+1了,然后NSString *str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
再次增加了引用,作用域结束的时候只是release了一次,这个变量在stringWithFormat内部放入了自动释放池,于是要在pool pop的时候才会再次release,真正的进行内存释放。
下面测试一下alloc之类的会怎样:
- 如果替换为mutableCopy,则在离开作用域的时候马上就销毁了:
NSMutableString *str = [@"a string object" mutableCopy];
输出:
Example1[41407:454952] viewWillAppear tracePtr: (null)
Example1[41407:454952] viewDidAppear tracePtr: (null)
- 如果替换为NSArray的alloc init方法也是会立刻release:
NSArray *arr = [[NSArray alloc] initWithObjects:@(1), nil];
tracePtr = arr;
输出:
Example1[41494:457063] viewWillAppear tracePtr: (null)
Example1[41494:457063] viewDidAppear tracePtr: (null)
- 如果替换为NSString的alloc init方法比较特殊,是不会release的:
NSString *str = [[NSString alloc] initWithString:@"a string object"];
//等同于NSString *str = @"a string object";
tracePtr = str;
输出:
Example1[41494:457063] viewWillAppear tracePtr: tracePtr: a string object
Example1[41494:457063] viewDidAppear tracePtr: tracePtr: a string object
这个我猜测大概是类似java里面的常量池,由系统来管理字符串字面量的释放之类的,和Array不太一样。
加入@autoreleasepool再测一下~
- stringWithFormat返回的autorelease对象会被加入到最近的autorelease pool也就是@autoreleasepool {}所在的page,在@autoreleasepool {}执行到结束的时候,就会把它包裹的新对象都从page拿出来执行一遍release,所以当运行到
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
的时候,str对象已经release过了。
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
tracePtr = str;
}
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
}
输出:
Example1[42032:469774] viewDidLoad tracePtr: (null)
Example1[42032:469774] viewWillAppear tracePtr: (null)
Example1[42032:469774] viewDidAppear tracePtr: (null)
- 在@autoreleasepool声明变量:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = nil;
@autoreleasepool {
str = [NSString stringWithFormat:@"%@", @"ssuuuuuuuuuuuuuuuuuuuu"];
tracePtr = str;
}
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
}
输出:
Example1[42055:470561] viewDidLoad tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[42055:470561] viewWillAppear tracePtr: (null)
Example1[42055:470561] viewDidAppear tracePtr: (null)
虽然str加入了autorelease pool,也就是在运行到@autoreleasepool结尾的时候会对str做release操作,相当于stringWithFormat的autorelease刚把对象放到自动释放池,自动释放池就做了pop操作执行了release,相当于抵消了stringWithFormat的autorelease。
但是str即使做了release计数-1,外面还有一个引用,所以引用数仍旧不为0,故而不会立刻释放,当运行完viewDidLoad的时候它的计数-1,会立刻进行释放。
6.我们再来最后试一下字面量的@autoreleasepool:
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
NSString *str = @"ssuuuuuuuuuuuuuuuuuuuu";
tracePtr = str;
}
NSLog(@"viewDidLoad tracePtr: %@", tracePtr);
}
输出:
Example1[42074:471180] viewDidLoad tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[42074:471180] viewWillAppear tracePtr: ssuuuuuuuuuuuuuuuuuuuu
Example1[42074:471180] viewDidAppear tracePtr: ssuuuuuuuuuuuuuuuuuuuu
看起来字面量好像即使用了@autoreleasepool也不会释放了,它大概是由系统管理吧,string和number应该是比较特殊的两种,但不用担心这种的内存问题,毕竟系统肯定会把这种管理好。
参考:
https://www.jianshu.com/p/8133439812d4
原理写的比较好:https://www.jianshu.com/p/d0558e4b0d21
https://www.jianshu.com/p/30c4725e142a
https://www.jianshu.com/p/5559bc15490d
https://www.jianshu.com/p/505ae4c41f31