美文网首页
iOS内存管理的前世今生

iOS内存管理的前世今生

作者: 小宝二代 | 来源:发表于2019-08-21 18:05 被阅读0次

    概述

    • 什么是内存管理

    应用程序内存管理是在程序运行时分配内存(比如创建一个对象,会增加内存占用)与清除内存(比如销毁一个对象,会减少内存占用)的过程。

    • 为什么要内存管理

    由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当APP所占用的内存较多时,系统就会发出内存警告,这时需要回收一些不需要再继续使用的内存空间,比如回收一些不再使用的对象和变量等。如果应用程序所占用的内存超过限制时,便会被系统强制关闭,所以我们需要对应用程序进行内存管理。

    • 内存管理的范围

    任何继承自NSObject的对象都需要管理内存,基本数据类型int、float、double、char、结构体struct以及枚举enum不需要管理内存。

    因为对象和其他数据类型在系统中的存储空间不一样,其它局部变量主要存放于栈中,而对象存储于堆中,当代码块结束时这个代码块中涉及的所有局部变量会被系统回收,指向对象的指针也被系统回收,此时对象已经没有指针指向,但依然存在于内存中,造成内存泄露。

    如下面一段代码:

    // 在栈内存中找一块区域,命名为a,用它来存放整数2
    int a = 2;
    // 在栈内存中找一块区域,命名为b,用它来存放整数4
    int b = 4;
    // 在栈内存中找一块区域,命名为obj,用它来存放指向NSObject *的指针
    // 在堆内存中找一块区域,用它来存放NSObject对象,指针变量obj指向NSObject对象的内存地址
    NSObject *obj = [[NSObject alloc] init];
    

    在内存中的表现形式如下:


    堆栈.png
    • 引用计数
    iOS内存管理无论是早期的MRC还是现在的ARC本质都是通过引用计数(Reference Counting)机制管理内存,当一个对象被创建出来时,内部都分配了4个字节的存储空间存放自己的引用计数,它的引用计数从0到1,当有外部对象对它进行强引用时,它的应用计数会+1,当该对象收到一条release消息时,它的引用计数会-1,当对象的引用计数为0时,对象将被释放,对象所占用的内存被回收。如图所示: 引用计数的内存管理.png

    在OC中提供了两种管理内存的方式,分别是MRC(Manual Reference Counting)手动引用计数ARC(Automatic Reference Counting)自动引用计数,在Xcode4.2以前程序员普遍使用MRC方式来管理内存,这也是内存管理的前世;使用Xcode4.2或以上版本,编译器将自动进行内存管理,也即是ARC,这就是内存管理的今生。下面将详细介绍MRC和ARC的内容。

    MRC

    MRC时代需要程序员手动管理对象的生命周期,也就是对象的引用计数由程序员来控制,什么时候retain,什么时候release,完全自己掌握。其实引用计数式内存管理我们也可以这样理解:

    • 自己生成的对象,自己所持有
    • 非自己生成的对象,自己也能持有
    • 不再需要自己持有的对象时释放
    • 非自己持有的对象无法释放

    这就是引用计数式内存管理的四个法则,这四个法则同样也适用于ARC,在四法则中出现了“生成”、“持有”、“释放”三个词,而在OC内存管理中还要加上“废弃”一词,各个词表示的OC方法如下表所示:

    对象操作 Objective-C方法 引用计数
    生成并持有对象 alloc/new/copy/mutableCopy等方法 +1
    持有对象 retain 方法 +1
    释放对象 release 方法 -1
    废弃对象 dealloc方法 0

    下面我们就来看一下在MRC下如何利用四法则进来内存管理。

    • 自己生成的对象,自己持有

    使用alloc/new/copy/mutableCopy开头的方法名意味着自己生成的对象自己持有

    /*
     * 自己生成并持有对象
     */
     NSObject *obj1 = [[NSObject alloc] init];
     NSObject *obj2 = [NSObject new];
    
     NSMutableString *str1 = [NSMutableString stringWithString:@"string"];
     NSString *str2 = [str1 copy];
     NSMutableString *str3 = [str1 mutableCopy];
    
    • 非自己生成的对象,自己也能持有

    在项目中我们可以使用类工厂的方法来取得一个对象,因为并不是使用alloc/new/copy/mutableCopy方法取得的,所以自己不是该对象的持有者,比如NSMutableArray类的array类方法,本文所说的“自己”可以理解为编程人员,“非自己”那便可以理解为系统或者编译器。

    /*
     * 取得非自己生成的对象,也可理解为取得系统生成的对象
     */
    id obj = [NSMutableArray array];
    /*
     * 自己持有对象
     */
    [obj retain];
    
    • 不再需要自己持有的对象时释放

    自己持有的对象,一旦不再需要,持有者有义务释放该对象,释放使用release方法。

    /*
     * 自己持有对象
     */
     id obj = [[NSObject alloc] init];
    /*
     * 释放对象
     */
     [obj release];
    /*
     * 指向对象的指针仍就被保留在obj这个变量中
     * 但对象已经释放,不可访问
     */
    
    • 无法释放非自己持有的对象

    对于用alloc/new/copy/mutableCopy方法生成并持有的对象,或是用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放,而由此以外所得到的对象绝对不能释放,倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。

    /*
     * 取得非自己生成的对象,也可理解为取得系统生成的对象
     * 但自己不持有对象
     */
    id obj = [NSMutableArray array];
    /*
     * 释放了非自己持有的对象
     * 导致程序崩溃
     */
    [obj release];
    /*
     * 我们也可以理解为该对象由系统生成由系统去释放
     * 谁创建,谁release,谁retain,谁release
     */
    

    以上就是MRC利用四法则进行内存管理的,这里我们再来看一下autorelease在内存管理中的功能。

    • autorelease

    autorelease提供了这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放(调用release方法)。我们使用该方法可以使取得的对象存在,但自己不持有对象,比如NSMutableArray类的array类方法就是通过autorelease实现的。

    - (id)object {
        
       /*
        * 自己生成并持有对象
        */
        id obj = [[NSObject alloc] init];
        
        /*
         * 把对象注册到自动释放池,交由系统管理,自己不再持有对象
         * 当自动释放池结束时该对象自动调用release
         */
        [obj autorelease];
    
        return obj; 
    }
    

    我们在自己书写类工厂方法时,也应该与系统处理方式相同,快速返回一个autorelease对象的方式具体如下:

    + (instancetype)person
    {
        // 使用self而不是使用Person是因为这样可以在子类调用该方法时会返回子类的对象
        return [[[self alloc] init] autorelease];
    }
    

    release和autorelease的区别如图所示:

    release和autorelease的区别.png

    autorelease的具体使用方法如下:

    1. 生成并持有NSAutoreleasePool对象
    2. 调用已分配对象的autorelease实例方法
    3. 废弃NSAutoreleasePool对象
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    id obj = [[NSObject alloc] init];
    [obj autorelease];
        
    [pool drain];
    
    NSAutoreleasePool对象的生存周期如图所示: NSAutoreleasePool对象的生存周期.png

    我们可以发现上面的介绍并没有考虑引用计数,只是利用四法则来进行内存管理,其实这背后的本质也就是引用计数式内存管理。我们可以看下面的例子:

    /*
     * 生成并持有对象,引用计数为1
     */
    NSObject *obj0 = [[NSObject alloc] init];
    NSLog(@"引用计数 = %lu 对象内存 = %p obj0指针变量内存 = %p", (unsigned long)[obj0 retainCount], obj0, &obj0);
    
    /*
     * retain引用计数+1
     */    
    NSObject *obj1 = [obj0 retain];
    NSLog(@"引用计数 = %lu 对象内存 = %p obj0指针变量内存 = %p obj1指针变量内存 = %p", (unsigned long)[obj0 retainCount], obj0, &obj0,&obj1);
    
    /*
     * release引用计数-1
     */      
    [obj1 release];
    NSLog(@"引用计数 = %lu 对象内存 = %p obj0指针变量内存 = %p obj1指针变量内存 = %p", (unsigned long)[obj0 retainCount], obj0, &obj0,&obj1);
    
    // 打印结果
    2019-08-20 17:10:48.348357+0800 MRC[12405:641180] 引用计数 = 1 对象内存 = 0x6000009646e0 obj0指针变量内存 = 0x7ffee88548b8
    2019-08-20 17:10:48.348486+0800 MRC[12405:641180] 引用计数 = 2 对象内存 = 0x6000009646e0 obj0指针变量内存 = 0x7ffee88548b8 obj1指针变量内存 = 0x7ffee88548b0
    2019-08-20 17:10:48.348558+0800 MRC[12405:641180] 引用计数 = 1 对象内存 = 0x6000009646e0 obj0指针变量内存 = 0x7ffee88548b8 obj1指针变量内存 = 0x7ffee88548b0
    

    我们可以看到alloc生成对象引用计数+1为1,retain又持有了对象,引用计数再+1为2,release释放对象,引用计数-1变为1。我们能看到[obj1 release]释放后指向对象的指针仍就被保留在obj1这个变量中,只是对象的引用计数-1了而已。

    对应的内存上的分配如下图所示: 堆栈.png

    ARC

    ARC(Automatic Reference Counting)自动引用计数是编译器的一个特性,能够自动管理OC对象内存生命周期。在ARC中你需要专注于写你的代码,retain 、release、autorelease操作交给编译器去处理就行了,编译器在编译阶段会自动地在适当的位置插入这些代码。这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量,如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。 MRC_ARC_示意图.png

    还记得上文我们提到的四个法则吗,它们在ARC有效时也是可行的,只是在源代码的书写上稍有不同,到底有什么样的变化呢?下面我们就来看一下ARC有效时编译源代码要遵守的规则。

    • ARC源代码编译规则
    1. 不能使用retain/release/retainCount/autorelease
    2. 不要显式调用dealloc,也即是可以实现dealloc方法,用于释放除了实例变量以外的其他资源,不需要调用[super dealloc],编译器会自动调用
    /* ARC无效 */
    - (void)dealloc {
        
        /* 该对象用的处理 */
        
        [super dealloc];
    }
    
    /* ARC有效 */
    - (void)dealloc {
        
        /*
         * 此处运行该对象被废弃时
         * 必须实现的代码
         */
    }
    
    1. 使用@autoreleasepool块替代NSAutoreleasePool来创建自动释放池
    2. 须遵守内存管理的方法命名规则,不能声明以new开头的属性,除非为该属性定义一个新的getter名称
    // 错误
    @property NSString *newTitle;
    // 正确
    @property (getter=theNewTitle) NSString *newTitle;
    

    我们知道在MRC中我们通过调用retain/release方法使对象的引用计数+1和-1,对象得到了很好的管理,那在ARC中是如何管理内存的呢?下面我们就介绍一下ARC中追加的所有权修饰符。

    • 所有权修饰符

    Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC有效时,id类型和对象类同C语言其他类型不同,其类型必须附加所有权修饰符。所有权修饰符一共4种:

    1. __strong修饰符
    2. __weak修饰符
    3. __unsafe_unretained修饰符
    4. __autoreleasing修饰符
    __strong修饰符

    __strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说,以下源代码中id变量,实际上被附加了所有权修饰符。

    id obj = [[NSObject alloc] init];
    

    等同于以下源代码

    id  __strong obj = [[NSObject alloc] init];
    

    __strong修饰符表示对对象的“强引用”,持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

       {
            /*
             * 因为变量obj为强引用
             * 所以自己持有对象
             */
            id __strong obj = [[NSObject alloc] init];
        }
        /*
         * 因为变量obj超出其作用域,强引用失效
         * 所以自动地释放自己持有的对象
         * 对象的所有者不存在,因此废弃该对象
         */
    

    等同于ARC无效时以下代码

      /* ARC无效 */
        {
            id obj = [[NSObject alloc] init];
    
            [obj release];
        }
    

    在取得非自己生成自己也不持有的对象时会如何呢?

       {
            /*
             * 取得非自己生成的对象
             * 因为变量obj为强引用
             * 所以自己持有对象
             */
            id __strong obj = [NSMutableArray array];
        }
        
        /*
         * 因为变量obj超出其作用域,强引用失效
         * 所以自动地释放自己持有的对象
         */
    

    等同于ARC无效时以下代码

        /* ARC无效 */
        {
            id obj =  [NSMutableArray array];
            
            [obj retain];
    
            [obj release];
        }
    

    __strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确地管理其对象的所有者,且看下面的代码

        /*
         * obj0持有对象A的强引用
         */
        id __strong obj0 = [[NSObject alloc] init]; /* 对象A */
     
        /*
         * obj1持有对象B的强引用
         */
        id __strong obj1 = [[NSObject alloc] init]; /* 对象B */
        
        /*
         * obj2不持有任何对象
         */
        id __strong obj2 = nil;
        
        /*
         * obj0持有由obj1赋值的对象B的强引用
         * 因为obj0被赋值,所以原先持有的对对象A的强引用失效
         * 对象A的所有者不存在,因此废弃对象A
         *
         * 此时,持有对象B的强引用的变量为obj0和obj1
         */
        obj0 = obj1;
        
        /*
         * obj2持有由obj0赋值的对象B的强引用
         *
         * 此时,持有对象B的强引用的变量为obj0,obj1和obj2
         */
        obj2 = obj0;
        
        /*
         * 因为nil被赋予了obj1,所以对对象B的强引用失效
         *
         * 此时,持有对象B的强引用的变量为obj0和obj2
         */
        obj1 = nil;
        
        /*
         * 因为nil被赋予了obj0,所以对对象B的强引用失效
         *
         * 此时,持有对象B的强引用的变量为obj2
         */
        obj0 = nil;
        
        /*
         * 因为nil被赋予了obj2,所以对对象B的强引用失效
         * 对象B的所有者不存在,因此废弃对象B
         */
        obj2 = nil;
    

    当然,即便是Objective-C类成员变量,也可以在方法参数上使用附有__strong修饰符的变量,且看下面代码

    @interface Test : NSObject 
    { 
        id __strong obj_;
    }
    - (void)setObject:(id __strong)obj;
    @end
    
    @implementation Test
    - (void)setObject:(id __strong)obj 
    {
        obj_ = obj;
    }
    
    @end
    
    // 使用Test类
    {
       /*
        * test持有Test对象的强引用
        */
        id __strong test = [[Test alloc] init];
        
        /*
         * Test对象的obj_成员变量持有NSObject对象的强引用
         */
        [test setObject:[[NSObject alloc] init]];
    }
    
    /*
     * 因为test变量超出其作用域,强引用失效
     * 所以自动释放Test对象
     * Test对象的所有者不存在,因此废弃该对象
     *
     * 废弃Test对象的同时
     * Test对象的obj_成员变量也被废弃
     * NSObject对象的强引用失效
     * 自动释放NSObject对象
     * NSObject对象的所有者不存在,因此废弃该对象
     */
    

    另外,__strong修饰符同后面要介绍的__weak修饰符和__autoreleasing修饰符一起,可以保证将附有这些修饰符的局部变量初始化为nil。

    id __strong obj0;
    id __weak obj1;
    id __autoreleasing obj2;
    

    以下源代码与上相同

    id __strong obj0 = nil;
    id __weak obj1 = nil;
    id __autoreleasing obj2 = nil;
    
    __weak修饰符

    貌似通过__strong修饰符编译器就能够完美地进行内存管理,但是并不能解决引用计数式内存管理中的“循环引用”的问题。下面代码就是一个循环引用。

    @interface Test : NSObject 
    { 
        id __strong obj_;
    }
    - (void)setObject:(id __strong)obj;
    @end
    
    @implementation Test
    - (void)setObject:(id __strong)obj 
    {
        obj_ = obj;
    }
    
    @end
    
    // 循环引用
    {
         /*
          * test0持有Test对象A的强引用
          */
         id test0 = [[Test alloc] init]; /* 对象A */
            
         /*
          * test1持有Test对象B的强引用
          */
         id test1 = [[Test alloc] init]; /* 对象B */
            
         /*
          * Test对象A的obj_成员变量持有Test对象B的强引用
          *
          * 此时,持有Test对象B的强引用变量为Test对象A的obj_和test1
          */
         [test0 setObject:test1];
            
         /*
          * Test对象B的obj_成员变量持有Test对象A的强引用
          *
          * 此时,持有Test对象A的强引用变量为Test对象B的obj_和test0
          */
         [test1 setObject:test0];
    }
    /*
      * 因为test0变量超出其作用域,强引用失效
      * 所以自动释放Test对象A
      *
      * 因为test0变量超出其作用域,强引用失效
      * 所以自动释放Test对象B
      *
      * 此时,持有Test对象A的强引用的变量为Test对象B的obj_
      *
      * 此时,持有Test对象B的强引用的变量为Test对象A的obj_
      *
      * Test对象A和Test对象B没有被废弃,发生内存泄漏
      *
      */
    

    __weak修饰符与__strong修饰符相反,提供弱引用,弱引用不能持有对象实例,使用_weak修饰符可以避免循环引用。把上面的代码成员变量obj用__weak修饰就可以了。

    @interface Test : NSObject 
    { 
        id __weak obj_;
    }
    - (void)setObject:(id __strong)obj;
    @end
    

    __weak修饰符还有另一个优点,在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态(空指针),因此通过检查附有__weak修饰符的变量是否为nil可以判断被赋值的对象是否已废弃。

    __unsafe_unretained修饰符

    __unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符,既不持有对象的强引用也不持有弱引用,若该修饰符所修饰的变量表示的对象被废弃,该变量不会被置为nil,变成了野指针,访问野指针会崩溃。

    id __unsafe_unretained obj1 = nil;
    {
        /*
         * 因为obj0变量为强引用
         * 所以自己持有对象
         */
        id __strong obj0 = [[NSObject alloc] init];
        
        /*
         * 虽然obj0变量赋值给obj1
         * 但是obj1变量既不持有对象的强引用也不持有弱引用
         */
        obj1 = obj0;
        
        NSLog(@"A: %@",obj1);
    }
    /*
     * 因为obj0变量超出其作用域,强引用失效
     * 所以自动释放自己持有的对象
     * 因为对象无持有者,所以废弃该对象
     */
    
    /*
     * 输出obj1变量表示的对象
     * obj1变量表示的对象已经被废弃
     * 访问野指针,程序崩溃
     */
    NSLog(@"B: %@",obj1);
    

    我们可以看到__weak修饰符的变量,当所表示的对象被废弃时会置为nil, __unsafe_unretained修饰符的变量不会置为nil,这就是二者的区别。__weak修饰符只能用于iOS5以上版本的应用程序,所以在iOS4的应用程序中必须使用__unsafe_unretained修饰符替代__weak修饰符。

    __autoreleasing修饰符

    将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。

    // ARC有效
    @autoreleasepool {
        id __autoreleasing obj = [[NSObject alloc] init];
    }
    

    等同于下面代码

    // ARC无效
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    id obj = [[NSObject alloc] init];
    [obj autorelease];
        
    [pool drain];
    
    可以理解为在ARC有效时用@autoreleasepool块替代NSAutoreleasePool类,用附有__autoreleasing修饰符的变量替代autorelease方法 @autoreleasepool和附有__autoreleasing修饰符的变量.png

    我们在介绍MRC的时候讲到使用类工厂方式取得的对象,编译器会自动将对象注册到autoreleasepool中,这在ARC中也是一样的,编译器会检查方法名是否已alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool。如下面代码:

    @autoreleasepool {
        
        /*
         * 因为变量obj为强引用,所以自己持有对象
         * 该对象由编译器判断其方法名后,自动注册到autoreleasepool
         */
        id __strong obj = [NSMutableArray array];
    }
    /*
     * 因为变量obj超出其作用域,强引用失效
     * 所以自动释放自己持有的对象
     *
     * 同时,随着@autoreleasepool块的结束,
     * 注册到autoreleasepool中的所有对象被自动释放
     * 因为对象的所有者不存在,所以废弃对象
     *
     */
    

    综上,我们会发现在ARC中不再观察对象的引用计数是否为0,只用考虑该对象是否有强指针指向,只要有一个强指针指向该对象,对象的所有者就不为0,就会一直存在于内存中。当对象的所有者不存在,对象就会被废弃,内存得以回收。

    所以,直接使用__weak或者__unsafe_unretained修饰变量指向一个刚创建的对象时,由于对象没有强指针指向,对象的所有者不存在,该对象会立即被释放。

    // 因为没有强指针指向该对象,该对象会立即被释放
    NSString * __weak string0 = [[NSString alloc]  initWithFormat:@"Hello"];
    
    // 因为没有强指针指向该对象,该对象会立即被释放
    NSString * __unsafe_unretained string1 = [[NSString alloc]  initWithFormat:@"Hello"];
    
    // 编译器给出警告
    warning: Assigning retained object to weak variable; object will be released after assignment
    
    • 属性修饰符

    属性中内存管理的修饰符与所有权修饰符的对应关系如下表:

    属性修饰符 所有权修饰符 用法
    assgin __unsafe_unretained修饰符 适用于基本数据类型
    retain __strong修饰符 适用于OC对象
    copy __strong修饰符(但是赋值的是被复制的对象) 适用于NSString和block
    strong __strong修饰符 适用于OC对象
    weak __weak修饰符 适用于OC对象(避免循环引用)

    其中strongweak是ARC中新增的两个属性修饰符,在MRC中属性的默认修饰符是assgin,在ARC中默认是strong。strong用于OC对象,相当于MRC中retain,weak用于OC对象,相当于MRC中的assgin,assgin用于基本数据类型,相当于MRC中的assgin。

    /*
     * 下面这句对于strong的示例
     * 与此同义: @property(retain) MyClass *myObject;
     */ 
    @property(strong) MyClass *myObject;
    
    /*
     * 下面这句对于weak的示例
     * 与此相似: @property(assign) MyClass *myObject;
     */ 
    @property(weak) MyClass *myObject;
    /*
     * 使用assign修饰的变量所指向的对象被释放,该指针会变成野指针
     * 使用weak修饰的变量所指向的对象被释放,该指针会变成空指针
     */ 
    

    如果想详细了解这几个属性修饰符可以看我的另一篇文章,iOS属性的修饰符(assign、retain、copy、weak、strong)

    内存管理检测

    未完待续。。。

    小结

    本文主要是对《iOS与OS X多线程和内存管理》这本书第一章自动引用计数的总结,结合自己的理解,由于本人知识和技术水平有限,文中不乏一些错误和不规范之处,欢迎大家留言批评指正。

    参考文献
    《iOS与OS X多线程和内存管理》书籍
    可能是史上最全面的内存管理文章
    iOS里的内存管理
    iOS性能调优之--内存管理

    相关文章

      网友评论

          本文标题:iOS内存管理的前世今生

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