[iOS] @autoreleasepool是干神马的

作者: 木小易Ying | 来源:发表于2019-10-13 07:22 被阅读0次

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

    #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执行后才归零。

    • 那么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

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

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

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

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

    • 假设当前线程只有一个AutoreleasePoolPage对象,对象的内存地址如下图:


      AutoreleasePoolPage开始
    • 然后当一个对象发送了autorelease消息,就是将当前这个对象加入到AutoreleasePoolPage的栈顶next指向的位置。

    • 每进行一次objc_autoreleasePoolPush调用时,runtime就会将当前的AutoreleasePoolPage加入一个哨兵对象,就会变成下面结构:


      objc_autoreleasePoolPush
    • 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的释放

    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

    相关文章

      网友评论

        本文标题:[iOS] @autoreleasepool是干神马的

        本文链接:https://www.haomeiwen.com/subject/ukmqmctx.html