iOS 开发每天分享优质文章iOS面试题+基础知识autoreleasepool

[iOS] @autoreleasepool是干神马的

2019-10-13  本文已影响0人  木小易Ying

首先我们先看个好玩的事情~

#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的时候,可能也不会立刻被释放,因为其实它的计数还没归零,当release执行后才归零。

还记得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

自动释放池AutoreleasePool是以一个个AutoreleasePoolPage组成,而AutoreleasePoolPage以双链表形成的自动释放池。

AutoreleasePoolPage中的每个对象都会开辟出虚拟内存一页的大小(也就是4096个字节),除了实例变量占据空间,其他的空间都用来存储autorelease对象的地址。

id * next指向的是栈顶对象的下一个位置,这样再放入新的对象的时候就知道放到哪个地址了,放入以后会更新next指向,让它指到新的空位。如果AutoreleasePoolPage空间被占满时,会创建一个AutoreleasePoolPage连接链表,后来的对象也会在新的page加入。

单向链表适用于节点的增加删除,双向链表适用于需要双向查找节点值的情况。这即是AutoreleasePoolPage以双链表的方式组合的原因。缺点就是空间占用较单链表大。

@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"];
  }
}

子线程中Autorelease的释放

  1. 子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。

  2. 在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都有autorelease的管理操作。

  3. 就算插入没有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,真正的进行内存释放。

所以哦,不是autoreleasepool可以自动监测对象的创建,而是你对象创建的时候被ARC默认加了return [obj autorelease],就被放进AutoReleasePage啦

下面测试一下alloc之类的会怎样:

  1. 如果替换为mutableCopy,则在离开作用域的时候马上就销毁了:
NSMutableString *str = [@"a string object" mutableCopy];

输出:
Example1[41407:454952] viewWillAppear tracePtr: (null)
Example1[41407:454952] viewDidAppear tracePtr: (null)
  1. 如果替换为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)
  1. 如果替换为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再测一下~

  1. 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)
  1. 在@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应该是比较特殊的两种,但不用担心这种的内存问题,毕竟系统肯定会把这种管理好。

最后有个小问题:子线程默认没有runloop,而autoreleasepool依赖于runloop,那么子线程没有autoreleasepool么?它的变量如何释放呢?
可以参考下下面的文章,总的而言就是最好自己创建一个
https://www.jianshu.com/p/90d08a99da20

参考:
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

上一篇下一篇

猜你喜欢

热点阅读