美文网首页iOS KitiOS学习收藏ios
iOS原理 AutoreleasePool的基本概念

iOS原理 AutoreleasePool的基本概念

作者: 东篱采桑人 | 来源:发表于2021-01-05 16:47 被阅读0次

    iOS原理 文章汇总

    前言

    一般情况下,对象在超出作用域时会立即release。比方说,在一个方法里创建一个局部对象:

    -(void)test{
        
        NSObject *obj = [[NSObject alloc] init];
    }
    

    test方法执行完,这个NSObject对象就会被release了。但有些时候,比如从工厂方法返回对象时,并不希望对象在超出作用域后立即release,这就需要通过AutoreleasePool来实现。

    AutoreleasePool的基本介绍

    AutoreleasePool(自动释放池)是OC中的一种内存管理机制,它持有释放池里的对象的所有权,在自动释放池销毁时,统一给所有对象发送一次release消息。通过这个机制,可以延迟对象的释放。

    • 延迟释放
    UIImage *img = [UIImage imageNamed:@"xxxx.png"];
    

    这个UIImage对象是在类方法imageNamed里创建完后再返回,对象的所有权归方法持有,如果不延迟释放,在方法结束时对象就被释放了,返回的就为nil。因此,需要将对象先加入AutoreleasePool,所有权归自动释放池持有,只有自动释放池销毁时才释放,这样UIImage对象才能在方法结束后正常返回。

    AutoreleasePool的创建方式

    通常使用@autoreleasepool {}代码块来手动创建一个自动释放池

    @autoreleasepool {
        //这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
        ... ...
    }
    

    这个代码块等价于

    {
        //创建一个AutoreleasePool对象
        __AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush(); 
        
        //这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
        ... ...    
    
       //给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
       objc_autoreleasePoolPop(atautoreleasepoolobj)
    }
    
    `{}`表示AutoreleasePool对象的作用域
    

    代码块的实现逻辑如下:

    • 先通过调用objc_autoreleasePoolPush函数来创建一个AutoreleasePool对象。
    • 然后给在代码块里创建的每个自动释放的对象发送一个autorelease消息,将这些自动释放的对象加入到AutoreleasePool对象里。
    • 最后在AutoreleasePool对象将要销毁时,通过调用objc_autoreleasePoolPop函数给池中每个自动释放的对象发送一次release消息,再销毁AutoreleasePool对象。

    注意区分AutoreleasePool对象自动释放的对象AutoreleasePool对象指的是实例化的一个自动释放池(本质也是对象),而 自动释放的对象是指被加入到这个池中的对象。
    AutoreleasePool的原理可阅读后面的底层分析一文。

    AutoreleasePool在Runloop中的创建和销毁

    通常情况下,在平时开发中不需要手动创建自动释放池,因为Runloop会自动创建和销毁AutoreleasePool对象。

    如上图所示,AutoreleasePoolRunloop中的创建和销毁的过程如下:

    • App启动后,系统在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

    • 第一个Observer监视一个事件:

      • Entry(即将进入Loop):调用objc_autoreleasePoolPush来创建自动释放池。
    • 第二个Observer监视了两个事件:

      • Before waiting(准备进入休眠):先调用objc_autoreleasePoolPop销毁旧的自动释放池,再调用objc_autoreleasePoolPush创建一个新的自动释放池。
      • Exit(即将退出Loop):调用objc_autoreleasePoolPop销毁自动释放池。
    • 第一个observeorder-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
      第二个Observerorder2147483647,优先级最低,保证销毁自动释放池发生在其他所有回调之后。

    也就是说,在一个RunLoop事件开始的时候会自动创建一个AutoreleasePool,在事件结束时再自动销毁。上面举例的imageNamed方法内部创建的对象也是加入到主线程RunLoop创建的AutoreleasePool中实现延迟释放的。因此,通常在开发中不需要开发者自己创建AutoreleasePool

    手动创建AutoreleasePool的场景

    虽然Runloop会自动创建和销毁自动释放池,但在有些情况下还是需要手动创建AutoreleasePool苹果官方文档建议在下面这三种情况下可能需要开发者创建自动释放池:

    • 编写不基于UI框架的程序,例如命令行工具。

      这一点的原因不是特别清楚,猜测是不基于UI框架的程序,可能不响应用户事件,导致不自动创建和销毁自动释放池。

    • 编写一个创建大量临时对象的循环。

      在循环内使用自动释放池块可以在下一次迭代之前释放这些对象,有助于减少应用程序的最大内存占用,即降低内存峰值

    • 编写非Cocoa程序时创建子线程。

      Cocoa程序中的每个线程都维护自己的自动释放池块堆栈。而编写一个非Cocoa程序,比如Foundation-only program,这时如果创建了子线程,若不手动创建自动释放池,自动释放的对象将会堆积得不到释放,导致内存泄漏。

    这里就第二个场景举例,来说明在循环内使用AutoreleasePool对于降低内存峰值的作用。

    //情况一:循环内不使用AutoreleasePool
    for (int i = 0; i<1000000; i++) {
    
        NSString *string = [NSString stringWithFormat:@"%@", @"0123456789"];
        NSLog(@" ==== %p", string);
    }
    
    //情况二:循环内使用AutoreleasePool
    for (int i = 0; i<1000000; i++) {
    
        @autoreleasepool {
    
            NSString *string = [NSString stringWithFormat:@"%@", @"0123456789"];
            NSLog(@" ==== %p", string);
        }
    }
    

    分别运行上面两种情况可以看到,在循环过程中,第一种情况的内存占用一直在增加,第二种情况的内存不会增加。这是因为:

    • 情况一:循环过程中,创建的NSString对象一直在堆积,只有在循环结束才一起释放,所以内存一直在增加。
    • 情况二:每一次迭代中都会创建并销毁一个AutoreleasePool,而每一次创建的NSString对象都会加入到AutoreleasePool中,所以在每次AutoreleasePool销毁时,NSString对象就会被释放,这样内存就不会增加。

    这个场景中AutoreleasePool是通过立即释放对象来降低内存峰值,而前面又说自动释放池用来延迟对象的释放,这两者其实不矛盾,本质是一样的,都是在自动释放池销毁时调用objc_autoreleasePoolPop来释放池中的对象。只不过调用的时机不同,这里的@autoreleasepool {}是在超出自己的作用域时就调用函数来销毁,而前面的是在Runloop休眠或退出时才调用函数来销毁,所以调用的时机不同,才会实现立即或者延迟释放的目的。

    @autoreleasepool {}的作用域指的就是前面提到的{},是AutoreleasePool对象的作用域。

    哪些对象可以被添加到自动释放池?

    MRC模式下,只要给对象发送autorelease消息,这个对象就会被添加到自动释放池。但在ARC模式下,是由编译器自动给对象发送autorelease消息,且不会给所有的对象都发送,只会给被编译器识别为自动释放的对象发送。一般来说,使用类方法(工厂方法)实例化的对象才是自动释放的对象,才能被添加到自动释放池,而使用new、alloc、copy关键字生成的对象和retain了的对象,不会被添加到自动释放池中。

    • UIImage对象为例
    for (int i = 0; i<1000000; i++) {
    
        @autoreleasepool {
    
            //1.自动释放的对象,需要被添加到自动释放池中
            UIImage *image = [UIImage imageNamed:@"test.png"];
            
            //2.非自动释放的对象,不能被添加到自动释放池中
            UIImage *image = [[UIImage alloc] init];
            
            NSLog(@" ==== image = %p", image);
        }
    }
    

    分别运行上面两种情况,第一种情况内存不会增加,第二种情况内存会增加。第二种情况虽然在@autoreleasepool {}中创建对象,但由于不是自动释放的对象,所以还是不能被添加到AutoReleasePool中,只能在循环结束一起释放。因此,在ARC模式下,只有自动释放的对象才能被添加到AutoReleasePool中,非自动释放的对象在超出作用域时会被立即释放。

    需要注意的是,自动释放的对象如果没有被添加到AutoReleasePool中,就会产生内存泄露。

    总结

    总得来说,关于AutoreleasePool的基本概念可以归纳以下几点:

    • AutoreleasePool(自动释放池)持有释放池里的对象的所有权,在自动释放池销毁时,统一给所有对象发送一次release消息。
    • 在一个RunLoop事件开始的时候会自动创建一个AutoreleasePool,在事件结束时再自动销毁,这样可以延迟对象的release
    • 也可以使用@autoreleasepool {}来手动创建自动释放池,在循环中使用可以立即释放对象,降低内存峰值。
    • MRC模式下,只要给对象发送autorelease消息,这个对象就会被添加到自动释放池。
    • ARC模式下,一般来说,使用类方法(工厂方法)实例化的对象才是自动释放的对象,才能被添加到自动释放池。自动释放的对象如果没有被添加到AutoReleasePool中,就会产生内存泄露。
    • ARC中,自动释放的对象由编译器自主识别并发送autorelease消息,添加到AutoReleasePool中。

    相关文章

      网友评论

        本文标题:iOS原理 AutoreleasePool的基本概念

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