美文网首页内存管理AutoreleasePool
AutoreleasePool详解和runloop的关系

AutoreleasePool详解和runloop的关系

作者: 高思阳 | 来源:发表于2019-02-14 22:53 被阅读2次

    内存管理一直是学习 Objective-C 的重点和难点之一,在实际的软件开发工作中,经常会遇见由于内存原因而导致的crash。而autorelease pool在内存管理中有着举足轻重的作用,只有理解了 autorelease pool 的原理,我们才算是真正了解了 Objective-C 的内存管理机制。下面我会从以下几个方面给大家讲解:

    · NSAutoreleasePool是什么?
    · NSAutoreleasePool的实现原理是什么?
    · NSAutoreleasePool何时释放?
    · 如何使用Autorelease Pool Blocks
    · AutoreleasePool与runloop和线程的关系
    · NSAutoreleasePool是什么?

    NSAutoreleasePool是什么?

    NSAutoreleasePool实际上是个对象引用计数自动处理器,在官方文档中被称为是一个类。Objective-C的对象(全部继承自NSObject),就是使用引用计数的方法来管理对象的存活,众所周知,当引用计数为0时,对象就被销毁了。操作非常简单,当对象被创建时,引用计数被设成1。可以给对象发送retain消息,让对象对自己的引用计数加1。而当对象接受到release消息时,对象就会对自己的引用计数进行减1,当引用计数到了0,对象就会调用自己的dealloc处理。当对象被加入到NSAutoreleasePool中,会对其对象retain一次,当NSAutoreleasePool结束时,会对其所有对象发送一次release消息。NSAutoreleasePool可以同时有多个,它的组织是个栈,总是存在一个栈顶pool,也就是当前pool,每创建一个pool,就往栈里压一个,改变当前pool为新建的pool,然后,每次给pool发送drain消息,就弹出栈顶的pool,改当前pool为栈里的下一个pool。

    NSAutoreleasePool的实现原理是什么?

    @autoreleasepool {} 在编译时 @autoreleasepool {} 被转换为一个__AtAutoreleasePool ,通常这个结构体会在初始化时调用 objc_autoreleasePoolPush()方法,在析构时调用 objc_autoreleasePoolPop () 方法。而这些方法都是对AutoreleasePoolPage的简单封。AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。我们使用的所以想深入理解AutoreleasePool必须首先了解AutoreleasePoolPage。一个空的 AutoreleasePoolPage 的内存结构如下图所示:

    空 AutoreleasePoolPage 的内存结构

    magic 用来校验 AutoreleasePoolPage 的结构是否完整;
    next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
    thread 指向当前线程;
    parent 指向父结点,第一个结点的 parent 值为 nil ;
    child 指向子结点,最后一个结点的 child 值为 nil ;
    depth 代表深度,从 0 开始,往后递增 1;
    hiwat 代表 high water mark 。

    一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,通过parent和child指针连接成链表,后来的autorelease对象在新的page加入。下面是某个线程的 autoreleasepool 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL (哨兵),即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为 coldPage() ,最后一个 AutoreleasePoolPage 结点为 hotPage() 。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象 objr3 的内存地址。

    某个线程的 autoreleasepool 堆栈的内存结构图

    到这里大家可能有点疑问。没关系,我下面会为大家详细讲解,首先我来介绍几个概念:

    1.POOL_SENTINEL(哨兵对象)

    你可能想要知道 POOL_SENTINEL 到底是什么,还有它为什么在栈中。首先回答第一个问题: POOL_SENTINEL 只是 nil 的别名。

    \#define POOL_SENTINEL nil
    

    在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

    int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        // do whatever you want
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
    }
    

    上面的 atautoreleasepoolobj 就是一个 POOL_SENTINEL。

    2.push 操作

    objc_autoreleasePoolPush() 函数本质上就是调用的 AutoreleasePoolPage 的 push 函数。

    void *objc_autoreleasePoolPush(void) 
    
    { 
    
        if (UseGC) return nil; 
    
        return AutoreleasePoolPage::push(); 
    
    } 
    

    因此,我们接下来看看 AutoreleasePoolPage 的 push 函数的作用和执行过程。一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址,在执行 pop 操作的时候作为函数的入参。

    static inline void *push()
    {
        id *dest = autoreleaseFast(POOL_SENTINEL);
        assert(*dest == POOL_SENTINEL);
        return dest;
    }
    

    push 函数通过调用 autoreleaseFast 函数来执行具体的插入操作。重点内容

    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);
        }
    }
    

    autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:
    · 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
    · 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
    · 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。

    每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。

    3.autorelease 操作

    通过 NSObject.mm 源文件,我们可以找到 -autorelease 方法的实现

    - (id)autorelease { 
    
        return ((id)self)->rootAutorelease(); 
    
    } 
    

    通过查看 ((id)self)->rootAutorelease() 的方法调用,我们发现最终调用的就是 AutoreleasePoolPage 的 autorelease 函数。

    id objc_object::rootAutorelease2()
    {
        assert(!isTaggedPointer());
        return AutoreleasePoolPage::autorelease((id)this);
    } 
    

    AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。

    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  *dest == obj);
        return obj;
    }
    

    4.POP操作

    同理,前面提到的 objc_autoreleasePoolPop(void *) 函数本质上也是调用的 AutoreleasePoolPage 的 pop 函数

    void objc_autoreleasePoolPop(void *ctxt)
    {
        if (UseGC) return;
        // fixme rdar://9167170
        if (!ctxt) return;
        AutoreleasePoolPage::pop(ctxt);
    }
    

    pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,就是pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。


    当前autoreleasepool 堆栈的内存结构

    对照之前的图,其实整个过程可以总结为:每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),当需要release时,objc_autoreleasePoolPop(哨兵对象)作为入参,根据传入的哨兵对象地址找到哨兵对象所处的page。在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

    此时,如果执行 pop(token1) 操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示:


    执行 pop(token1) 操作

    NSAutoreleasePool何时释放?

    当别人问你NSAutoreleasePool何时释放?你回答“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。

    小实验。

    - (void)viewDidLoad { 
    
        [super viewDidLoad]; 
    
        // 场景 1 
    
        NSString *string = [NSString stringWithFormat:@"1234567890"]; 
    
        self.string_weak = string; 
    
         //场景 2 
    
    //        @autoreleasepool { 
    
    //            NSString *string = [NSString stringWithFormat:@"1234567890"]; 
    
    //        _string_weak = string; 
    
    //        } 
    
    //    NSLog(@"string: %@",_string_weak);
    
    // 场景 3
    1
    
    
    //        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); 
    
    } 
    

    思考得怎么样了?相信在你心中已经有答案了。那么让我们一起来看看 console 输出:

    //情景1
    2016-10-27 20:29:06.069 AutoreleasePoolTest[974:138797] string: 1234567890
    2016-10-27 20:29:06.070 AutoreleasePoolTest[974:138797] string: 1234567890
    2016-10-27 20:29:06.076 AutoreleasePoolTest[974:138797] string: (null)
    
    //情景2
    2016-10-27 20:31:58.836 AutoreleasePoolTest[1003:141350] string: (null)
    2016-10-27 20:31:58.837 AutoreleasePoolTest[1003:141350] string: (null)
    2016-10-27 20:31:58.844 AutoreleasePoolTest[1003:141350] string: (null)
    
    //情景3
    2016-10-27 20:33:21.699 AutoreleasePoolTest[1014:142465] string: 1234567890
    2016-10-27 20:33:21.699 AutoreleasePoolTest[1014:142465] string: (null)
    2016-10-27 20:33:21.703 AutoreleasePoolTest[1014:142465] string: (null)
    

    是不是和你所想一样?我们一起来分析下为什么会得到这样的结果。

    分析:当使用 [NSString stringWithFormat:@"1234567890"]创建一个对象时,这个对象的引用计数为 1 ,并且这个对象被系统自动添加到了当前的主线程的 autoreleasepool 中,autoreleasepool和strong同时拥有这个对象,所以引用计数为2。(如果这里直接输出 _string_weak的引用计数为3,因为 weak 的 retainCount 管理是独立的)。当程序执行完 - (void)viewDidLoad 时,string 释放,字符串对象引用计数-1。当执行- (void)viewWillAppear方法时,字符串对象引用计数为1。主线程的runloop一次迭代并没有结束,字符引用计数大于0,所以此时输出仍然有字符。而执行- (void)viewDidAppear时。runloop结束,autoreleasepool释放。字符串对象引用计数为0;所以此时字符串为空。

    情景2:

    当使用 [NSString stringWithFormat:@"1234567890"]创建一个对象时,string和autoreleasepool块同时持有,引用计数为2,当出来代码块后,string和autoreleasepool都会被释放。引用计数-2为0.字符串被释放,所以下面的输出全为空。

    情景3:

    申请字符串后,同时被string局部变量和代码块共有,引用计数为2,当出了autoreleasepool代码块的作用域时,字符串引用计数-1;由于string作用域为- (void)viewDidLoad 。所以第一次输出时字符串引用计数为1;当出了- (void)viewDidLoad 引用计数-1为0;此时字符串被释放。接下来两次输出都会为空。

    如何使用Autorelease Pool Blocks

    苹果文档介绍:很多程序会创造大量的临时对象,这些对象一直占用内存block的结束。大多情况下临时对象会一直在内存中聚集,直到当前的一次runloop迭代结束这样对内存造成很大的负担。苹果用了一段代码举例说明

    NSArray *urls = <# An array of file URLs #>; 
    
    for (NSURL *url in urls) { 
    
        @autoreleasepool { 
    
            NSError *error; 
    
            NSString *fileContents = [NSString stringWithContentsOfURL:url 
    
                                             encoding:NSUTF8StringEncoding error:&error]; 
    
            /* Process the string, creating and autoreleasing more objects. */ 
    
        } 
    
    }
    

    每次循环代码块结束,里面的临时对象也会释放,这样就很好的解决了内存占用的问题。根据苹果官方文档中对 Using Autorelease Pool Blocks 的描述,我们知道在下面三种情况下是需要我们手动添加 autoreleasepool 的:
    · 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
    · 如果你编写的循环中创建了大量的临时对象;
    · 如果你创建了一个辅助线程。

    AutoreleasePool与runloop和线程的关系

    根据苹果官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。子线程的runloop需要自己手动创建,如果子线程的runloop没有任何事件,runloop会马上退出。(曾经我在写异步网络请求时想利用runloop来暂停线程来执行回调,但是由于没有添加任何事件源,导致runloop马上结束。网络请求失败)如果在每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。我们上面提到的场景 1 中创建的 autoreleased 对象就是被系统添加到了这个自动创建的 autoreleasepool 中,并在这个 autoreleasepool 被 drain 时得到释放。另外,NSAutoreleasePool 中还提到,每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。

    原文:https://blog.csdn.net/mr_xiaojie/article/details/52953807

    相关文章

      网友评论

        本文标题:AutoreleasePool详解和runloop的关系

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