《Objective-C高级编程》内存管理

248 阅读12分钟

前言

ARC是iOS 5推出的新功能,全称叫 ARC(Automatic Reference Counting)。简单地说,就是代码中自动加入了retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。 在LLVM编辑器中设置ARC为有效状态,就无需再次键入retain或者是release代码。编译器将自动进行内存管理,在ARC之前是如何手工进行内存管理的呢?

内存管理 / 引用计数

1. 概要

Objective-C 中的内存管理,也就是引用计数。可以用开关灯的例子来说明。 这里假设需要照明的人数 = N; (1)第一个人进入办公室,N加 1,计数值从 0 变为了 1 ,因此要开灯。 (2)之后每当有人进入办公室,N就加 1,如计数值从 1 变成 2。 (3)每当有人下班离开办公室,N就减 1,如计数值从 2 变成 1。 (4)最后一个人下班离开办公室时,N减 1,计数值从 1 变成了 0 ,因此要关灯。

办公室照明系统.png

在Objective-C中,对象相当于办公室的照明设备,对象的使用环境相当于上班进入办公室的人。在概念上就是使用对象的环境。

对办公室照明设备所做的动作和对Objective-C 对象所做的动作对比:

对照明设备所做的动作 对Objective-C 对象所做的动作
开灯 生成对象
需要照明 持有对象
不需要照明 释放对象
关灯 废弃对象

使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理,同样使用引用计数功能,对象也能得到很好的管理,这就是Objective-C的内存管理。

引用计数的内存管理.png

2. 内存管理的思考方式

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

引用计数式的内存管理的思考方式仅此而已。按照这个思路,完全不必考虑引用计数。

对象操作 Objective-C 方法
生成并持有对象 alloc/new/copy/mutableCopy等方法
持有对象 retain方法
释放对象 release方法
废弃对象 dealloc方法

Cocoa框架中Foundation框架类库的NSObject类担负内存管理的职责。Objective-C内存管理中的alloc/retain/release/dealloc方法分别指代NSObject类的alloc类方法、retain实例方法、release实例方法和dealloc实例方法。

2.1 自己生成的对象,自己所持有

使用以下名称开头的方法名意味着自己生成的对象只有自己持有:

  • alloc
  • new
  • copy
  • mutableCopy
    /*
     * 自己生成并持有对象
     */
    id obj = [[NSObject alloc]init];
    
    /*
     * 自己持有对象
     */

使用NSObject类的alloc类方法就能自己生成并持有对象,指向生成并持有对象的指针被赋给变量obj,[NSObject new][[NSObject alloc] init]一样。 copymutableCopy方法生成并持有对象的副本。

2.2 非自己生成的对象,自己也能持有

用alloc/new/copy/mutableCopy以外的方法取得的对象因为非自己生成并持有,所以自己不是该对象的持有者。

    /*
     * 取得非自己生成并持有的对象
     */
    id obj2 = [NSMutableArray array];
    
    /*
     * 取得的对象存在,但自己不持有对象
     */

    [obj2 retain];
    
    /*
     * 自己持有对象
     */
2.3 不再需要自己持有的对象时释放

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

    /*
     * 自己生成并持有对象
     */
    id obj2 = [[NSObject alloc]init];
    
    /*
     * 自己持有对象
     */
    
    [obj2 release];
    
    /*
     * 释放对象
     * 指向对象的指针仍然被保留在变量obj中,貌似能够访问
     * 但对象一经释放绝对不可访问
     */

用alloc/new/copy/mutableCopy 方法生成并持有的对象,或者用retain方法持有的对象,一旦不再需要,务必要用release方法进行释放。

- (id)allocObject{
    /*
     * 自己生成并持有对象
     */
    id obj = [[NSObject alloc]init];
    
    /*
     * 自己持有对象
     */
    return obj;
}

如上例所示,原封不动的返回用alloc方法生成并持有的对象,就能让调用方也持有该对象,请注意allocObject这个名称是符合前文命名规则的。意味着自己生成并持有对象。

2.4 非自己持有的对象无法释放

对于用alloc、new、copy、mutableCopy方法生成并持有的对象,或者用retain方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。

   /*
     * 自己生成并持有对象
     */
    id obj2 = [[NSObject alloc]init];
    
    /*
     * 自己持有对象
     */
    
    [obj2 release];
    
    /*
     * 释放对象
     * 指向对象的指针仍然被保留在变量obj中,貌似能够访问
     * 但对象一经释放绝对不可访问
     */

    [obj2 release];
    
    /*
     * 访问已经废弃的对象,发生崩溃
     */

ARC规则

实际上“引用计数式内存管理”的本质部分在ARC中并没有改变,就像“自动引用计数”这个名称表示的那样,ARC只是自动地帮助我们处理“引用计数”的相关部分。

同一个应用程序中按文件单元可以选择ARC有效/无效

1. 所有权修饰符

Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 所谓对象类型就是指向NSObject这样的Objective-C类的指针,id类型用于隐藏对象类型的类名部分。 ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符,所有权修饰符一共有4种。

  • __strong 修饰符
  • __weak 修饰符
  • __unsafe_unretained 修饰符
  • __autoreleasing 修饰符
1.1 __strong 修饰符

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

id obj = [[NSObject alloc]init];

id和对象类型在没有明确指定所有权修饰符时,默认为__strong 修饰符:

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

对象所有者和对象的生命周期:

   {
      /*
       * 自己生成并持有对象
       */
        
     id __strong obj = [[NSObject alloc]init];
     
      /*
       * 因为变量obj 为强引用
       * 所以自己持有对象
       */
    }
    
    /*
     * 因为变量 obj 超出其作用域,强引用失效
     * 所以自动的释放自己持有的对象
     * 对象的所有者不存在,因此废弃该对象
     */
/*和上面一样的道理*/
id __strong obj = [NSMutableArray array];

当然,附有__strong修饰符的变量之间可以相互赋值。

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

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

正如苹果宣称的那样,通过__strong修饰符,不必再次键入retain或者release,完美的满足了“引用计数式内存管理的思考方式”:

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

前两项,只需通过对带__strong修饰符的变量赋值便可达成。通过废弃带__strong修饰符的变量(变量作用域结束或是成员变量所属对象废弃)或者对变量赋值,都可以做到“不再需要自己持有的对象时释放”。最后一项“ 非自己持有的对象无法释放”,由于不必再次键入release,所以原本就不会执行,这些都满足于引用计数式内存管理的思考方式。

因为id类型和对象类型的所有权修饰符默认为__strong修饰符,所以不需要写上__strong。使ARC有效及简单的编程遵循了Objective-C 内存管理的思考方式。

1.2 __weak 修饰符

看起来好像通过__strong修饰符编译器就能完美的进行内存管理,但遗憾的是它不能解决引用计数式内存管理中必然会发生的 “循环引用” 的问题。

循环引用

下面👇是一个循环引用的例子:

   {
        id text0 = [[Text alloc]init];/*对象A*/
        
        /*
         * text0 持有对象A的强引用
         */
        
        id text1 = [[Text alloc]init];/*对象B*/
        
        /*
         * text1 持有对象B的强引用
         */
        
        [text0 setObj:text1];
        
        /*
         * Text对象A的成员变量 obj_ 持有Text对象B的强引用
         * 此时,持有Text对象B强引用的变量为
         * Text对象A的 obj_ 和 text1
         */
        
        [text1 setObj:text0];
        
        /*
         * Text对象B的成员变量 obj_ 持有Text对象A的强引用
         * 此时,持有Text对象A强引用的变量为
         * Text对象B的 obj_ 和 text0
         */
    }
    
    /*
     * 因为 text0 变量超出其作用域,强引用失效
     * 所以自动释放Text对象A
     *
     * 因为 text1 变量超出其作用域,强引用失效
     * 所以自动释放Text对象B
     *
     * 此时,持有对象A强引用的变量为
     * Text对象B的 obj_
     *
     * 此时,持有对象B强引用的变量为
     * Text对象A的 obj_
     *
     * 发生内存泄漏
     */

循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。 上面这段代码的本意是赋予变量text0 的对象A和赋予变量text1的对象B在超出其作用域时被释放,即在对象不被任何变量持有的状态下予以废弃,但是循环引用使得对象不能被再次废弃。

__weak修饰符与__strong修饰符相反,提供弱引用,弱引用不能持有对象实例,也就是说,使用__weak修饰符可以避免循环引用。

- (void)text1{
    id __weak tmpObj;
    {
        /*
         * 自己生成并持有对象
         */
        
        id __strong obj = [[NSObject alloc]init];
        
        /*
         * 因为obj变量为强引用
         * 所以自己持有对象
         */
        
        tmpObj = obj;
        
        /*
         * tmpObj变量持有对象的弱引用
         */
        
        NSLog(@"%@",tmpObj);
        
        /*
         * 输出tmpObj变量持有的弱引用的对象
         */
        
    }
    /*
     * 因为tmpObj变量超出其作用域,强引用失效
     * 所以自动释放自己持有的对象
     * 因为对象无持有者,所以废弃该对象
     *
     * 废弃对象的同时
     * 持有该对象弱引用的tmpObj变量的弱引用失效,nil赋值给tmpObj
     */
    NSLog(@"%@",tmpObj);
}

打印结果如下:

2017-12-26 15:38:43.112704+0800 MemoryManageStudy[3991:3494215] <NSObject: 0x1c000dd00>
2017-12-26 15:38:43.112830+0800 MemoryManageStudy[3991:3494215] (null)
1.3 __unsafe_unretained 修饰符

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。尽管ARC式的内存管理是编译器的工作,但附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,这一点在使用时要注意。

- (void)text5{
    id __unsafe_unretained obj1 = nil;
    {
        /*
         * 自己生成并持有对象
         */
        
        id __strong obj = [[NSObject alloc]init];
        
        /*
         * 因为obj变量为强引用
         * 所以自己持有对象
         */
        
        obj1 = obj;
        
        /*
         * 虽然obj变量赋值给obj1
         * 但是obj1变量既不持有对象的强引用,也不持有对象的弱引用
         */
        
        NSLog(@"%@",obj1);
    }
    
    /*
     * 因为obj变量超出其作用域,强引用失效
     * 所以自动释放自己持有的对象
     * 因为对象无所有者,所以废弃该对象
     */
    
    //访问已经废弃的对象,发生崩溃
    NSLog(@"%@",obj1);
}
1.4 __autoreleasing 修饰符

ARC有效时,虽然autorelease无法直接使用,但实际上,ARC有效时autorelease功能是起作用的。 要通过将对象赋值给附加了__autoreleasing修饰符的变量来替代调用autorelease方法,对象赋值给附加了__autoreleasing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,即对象被注册到autoreleasepool

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

编译器会检查方法名是否以alloc、new、copy、mutableCopy开始,如果不是则自动将返回值的对象注册到autoreleasepool。 同样的,@autoreleasepool块也能嵌套使用。

@autoreleasepool{        
        @autoreleasepool{ 
            @autoreleasepool{
                id __autoreleasing obj = [NSMutableArray array];
            }
        }
    }

2. 规则

在ARC有效的情况下编译源代码,必须遵守一定的规则,下面就是具体的ARC的规则。

  • 不能使用retain、release、retainCount、autorelease
  • 不能使用NSAllocateObject、NSDeallocateObject
  • 必须遵守内存管理的方法命名规则
  • 不要显示调用dealloc
  • 使用@autoreleasepool块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体的成员
  • 显示转换 id 和 void*

3. 属性

当ARC有效时,Objective-C类的属性也会发生变化。

属性声明的属性 所有权修饰符
assign __unsafe_unretained修饰符
copy __strong修饰符(但是赋值的是被复制的对象)
retain __strong修饰符
strong __strong修饰符
unsafe_unretained __unsafe_unretained修饰符
weak __weak修饰符