开发总结

OC高级编程iOS内存管理-第1章-自动引用计数

2018-03-28  本文已影响38人  凡几多

自动引用计数


  • 什么是自动引用计数
  • 内存管理/引用计数
  • ARC规则
  • ARC的实现

1.1 什么是自动引用计数

  • ARC和MRC的区别:
    MRC:(Manual Reference Counting)也就是非ARC,在Xcode4之前,Object_C的内存管理就需要开发人员手动维护。
    ARC:(Automatic Reference Counting)也就是ARC,翻译成中文就是:【自动引用计数】,不需要开发人员手动维护,系统会在合适的时候调用内存管理方法。

顾名思义,自动引用计数(ARC,Automatic Reference Counting)是指内存管理中对引用采取自动计数的技术。以下摘自苹果的官方说明。
在Objective-C中采用Automatic Reference Counting(ARC)机制,让编辑器来进行内存管理。在新一代的Apple LLVM编辑器中设置ARC为有效状态,就无需再次键入retain或release代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流程运行,速度也将大幅提升。

1.2 内存管理/引用计数

可以用开关房间的灯为例来说明引用计数的机制。
加入办公室里的照明设备只有一个。上班进入办公室的人需要照明,所以要开灯,下班离开办公室的人不需要照明,所以要把灯关掉。若是很多人上下班,每个人都开灯或者关灯,那么办公室的情况又将如何呢?最早下班离开的人如果关了灯,那办公室还没走的所有人都将处于一片黑暗之中。
解决这一问题的办法是使办公室在还有至少1人的情况下保持开灯状态,而在无人时保持关灯状态。

(1)最早进入办公室的人开灯。
(2)之后进入办公室的人,需要照明。
(3)下班离开办公室的人,不需要照明。
(4)最后离开办公室的人关灯(此时已无人需要照明)。

为判断是否还有人在办公室,这里导入计数功能来计算“需要照明的人数”。下面让我们看看这一功能如何运作的。

(1)第一个人进入办公室,“需要照明的人数”加1。计数值从0变成了1,因此要开灯。
(2)之后每当有人进入办公室,“需要照明的人数”就加1.如计数值从1变成2。
(3)每当有人下班离开办公室,“需要照明的人数”就减1。如计数值从2变成1。
(4)最后一个人下班离开办公室时,“需要照明的人数”减1。计数值从1变成了0,因此要关灯。

如下图: 引用计数图1.png

在Objective-C中,“对象”相当于办公室的照明设备。“对象的使用环境”相当于上班进入办公室的人。上班进入办公室的人对办公室照明设备发出的动作,与Objective-C中的对应关系如表1-1所示。

表1-1 对办公室照明设备所做的动作和对Objective-C的对象所做的动作
对照明设备所做的动作 对Objective-C对象所做的动作
开灯 生成对象
需要照明 持有对象
不需要照明 释放对象
关灯 废弃对象
如图: 引用计数图2.png

内存管理的思考方式

首先来学习引用计数式内存管理的思考方式。看到“引用计数”这个名称,我们便会不自觉的联想到“某处有某物多少多少”而将注意力放到计数上。但其实,更加客观、正确的思考方式是:

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。
    上文出现了“生成”、“持有”、“释放”三个词。而在Objective-C内存管理中还要加上“废弃”一词,各个词表示的Objective-C方法如表1-2。
表1-2 对象操作与Objective-C方法的对应
对照明设备所做的动作 对Objective-C对象所做的动作 Object-C方法
开灯 生成对象 alloc/new/copy/mutableCopy等方法
需要照明 持有对象 retain方法
不需要照明 释放对象 release方法
关灯 废弃对象 dealloc方法

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

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

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

copy方法利用基于NSCopying方法约定,由各类实现的copyWithZone:方法生成并持有对象的副本。与copy方法类型,mutableCopy方法利用基于NSMutableCopying方法约定,由各类实现的mutableCopyWithZone:方法生成并持有对象的副本。两者的区别在于,copy方法生成不可变更的对象,而mutableCopy方法生成可变更的对象。

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

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

[obj retain];
/*
* 自己持有对象
*/

通过retain方法,非自己生成的对象跟用alloc/new/copy/mutableCopy方法生成并持有的对象一样,成为了自己所持有的。

不再需要自己持有的对象时释放

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

[obj release];
/*
* 释放对象
* 对象一经释放绝对不可访问。
*/

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

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

[obj release];
/*
* 对象已释放
*/

[obj release];
/*
* 释放之后再次释放已非自己持有的对象!应用程序崩溃!
* 崩溃情况:
* 再度废弃一经废弃了的对象时崩溃,访问一经废弃的对象时崩溃。 
*/

或者在“取得的对象存在,但自己不持有对象”时释放,也会造成程序崩溃。
以上四项内容,就是“引用计数式内存管理”的思考方式。

关于引用计数器:

每个OC对象都有自己的引用计数器,是一个整数表示对象被引用的次数,即现在有多少东西在使这个对象。对象刚被创建时,默认计数器值为1,当计数器的值变为0时,则对象销毁。
在每个OC对象内部,都专门有4个字节的存储空间来存储引用计数器。

引计数器的作用

判断对象要不要回收的唯一依据就是计数器是否为0,若不为0则存在。
操作:
当一个对象的引用计数器为0时,那么它将被销毁,其占用的内存被系统回收。一旦对象被回收了,那么他所占据的存储空间就不再可用,坚持使用会导致程序崩溃(野指针错误)。

#pragma mark    -------------C语言里面malloc 用法-------------------

char *p = (char *)malloc(100);//C语言的动态分配  手动申请了100个字节的存储空间
strcpy(p, "niming");//将字符串存储到 指针变量p指向的内存空间 
puts(p);//输出
free(p);//释放

#pragma mark    ------------- OC内存管理 release 实例----------------
/*
 * 自己生成并持有对象
 */
 id obj = [[NSObject alloc] init];
 // 或
 id obj = [NSObject new];
/*
 * 释放对象
 * 对象不可再被访问
 */
  [obj release];
内存管理基本原则(黄金法则)

1.如果你通过alloc,new,copy等开头的方法,来创建了一个对象,那么你就必须调用release或者autorelease方法
2.谁创建,谁release (个人顾个人原则)
3.谁retain,谁release(只要你调用了retain,无论这个对象是如何生成的,你都要调用release)
总结:有始有终,有加就应该有减,曾经让某个对象计数器+1,就应该让其在最后-1

dealloc(对象的销毁)
/*
*(1)一定要[super dealloc], 且要放到最后
*(2)对self(当前)所拥有的的其他对象做一次release操作
*/
 - (void)dealloc {
  [obj release];
  [super dealloc];
 }

当一个对象的引用计数器为0时,那么它将被销毁,其占用的内存被系统回收。当对象被销毁时,系统会自动向对象发送一条dealloc消息,一般会重写dealloc方法,在这里释放相关的资源,dealloc就像是对象的“临终遗言”。一旦重写了dealloc方法就必须调用[super dealloc],并且放在代码块的最后调用(不能直接调用dealloc方法)。

给对象发送消息,进行相应的计数器操作。
retain消息:使计数器+1,该方法返回对象本身
release消息:使计数器-1(并不代表释放对象)
retainCount消息:获得对象当前的引用计数器值

autorelease

说到Objective-C内存管理,就不能不提autorelease。
顾名思义,autorelease就是自动释放。这看上去很像ARC,但实际上它更类似于C语言中自动变量(局部变量)的特性。
我们来复习一下C语言的自动变量。程序执行时,若某自动变量超出其作用域,该自动变量将被自动废弃。
autorelease会像C语言的自动变量那样对待对象实例。当超出其作用域(相当于变量作用域)时,对象实例的release实例方法被调用。另外,同C语言的自动变量不同的是,编程人员可以设定变量的作用域。

#pragma mark -- ios 5.0之前的创建方式
    
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool darin];// 此处等同于 “[obj release]”

#pragma mark -- 5.0之后

    @autoreleasepool {//代表开始创建自动释放池
        
        // insert code here...
        NSLog(@"Hello, World!");
        
        Person *person = [[Person alloc] init];
        
        [person autorelease];
        
    }//结束自动释放池

1.系统自带的方法中,如果不包含 alloc new copy等关键字,则这些方法返回的对象 都是 autorelease
如: [NSDate date];
如 :

id array = [NSMutableArray arrayWithCapacity:1];

此源代码等同于以下源代码。

id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];

1.3 ARC规则

“引用计数式内存管理”本质在ARC中并没有变化,ARC只是自动地帮助我们处理“引用计数”的相关部分。在编译单位上,可设置ARC有效或无效。指定编译器属性为“-fobjc-arc”。

与Java的对比

在java中,内存管理由JVM完全负责,java中的“垃圾回收器”负责自动回收无用对象占据的内存资源,这样可以大大减少程序猿在内存管理上花费的时间,可以更集中于业务逻辑和具体功能实现;但这并不是说java有了垃圾回收器程序猿就可以高枕无忧,将内存管理抛之脑外了!一方面,实际上java中还存在垃圾回收器没法回收以某种“特殊方式”分配的内存的情况;另一方面,java的垃圾回收是不能保证一定发生的,除非JVM面临内存耗尽的情况。所以java中部分对象内存还是需要程序猿手动进行释放,合理地对部分对象进行管理可以减少内存占用与资源消耗。

java的对象内存状态&&引用形式及回收时机,如下图:
对象状态转换图.png
java对象的四种引用
  • 强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :Person person = new Person(“sunny”); 不管系统资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到。
  • 软引用 :通过SoftReference类实现,eg : SoftReference p = new SoftReference(new Person(“Rain”));内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。
  • 弱引用 :通过WeakReference类实现,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收
  • 虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现
点我了解更多JAVA的内管管理机制
再次回到OC——>

所有权修饰符

id __strong obj0;  
id __weak obj1;  
id __autoreleasing obj2;  
// __strong,__weak,__autoreleasing可以保证附有这些修饰符的自动变量初始化为nil  
  
// 上面源码和下面效果等同  
id __strong obj0 = nil;  
id __weak obj1 = nil;  
id __autoreleasing obj2 = nil;  
__strong修饰符

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

id obj = [[NSObject alloc] init];

id和对象类型在没有明确指定所有权修饰符时,默认为__strong修饰符。上面的源代码与以下相同。

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

该源代码在ARC无效时的表述为:

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

该源代码一看则明,目前在表面上没有任何变化。再看看下面的代码。

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

此源代码明确指定了C语言的变量作用域。ARC无效时,该源代码可记述如下:

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

为了释放生词并持有的对象,增加了调用release方法的代码。动作同先前ARC有效时动作完全一样。
如“strong”这个名称所示,__strong修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

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

此处,对象的所有者和对象的生存周期是明确的。那么,在取得非自己生成并持有的对象时又会如何呢?

{
     id ___strong obj = [NSMutableArray array];
}

在NSMutableArray 类的 array 类方法的源代码中取得非自己生成的对象,具体如下:

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

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

正如苹果宣称的那样,通过__strong修饰符,不必再次键入retain或release,完美地满足了“引用计数式内存管理的思考方式”。因为id类型和对象类型的所有权修饰符默认为__strong修饰符,所以不需要写上“__strong”。使ARC有效及简单的编程遵循了Objective-C内存管理的思考方式。

__weak修饰符

__weak用来避免“循环引用”问题。

例如前面出现带有__strong 修饰符的成员变量在持有对象时,很容易发生循环引用。

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

@implementation Test
- (id)init
{
    self = [super init];
    return self;
}
- (void)setObject:(id __strong)obj
{
    obj_ = obj;
}

以下为循环引用。

{
    id test0 = [[Test alloc] init]; // 对象A 。test0持有Test对象A的强引用

    id test1 = [[Test alloc] init]; // 对象B。 test1持有Test对象B的强引用

    [test0 setObject:test1];
    /*
     * Test 对象 A 的 obj_成员变量持有Test对象 B 的强引用。
     *
     * 此时,持有Test对象 B 的强引用的变量为
     * Test 对象 A 的obj_和test1。
     */

    [test1 setObject:test1];
     /*
     * Test 对象 B 的 obj_成员变量持有Test对象 A 的强引用。
     *
     * 此时,持有Test对象 A 的强引用的变量为
     * Test 对象 B 的obj_和test0。
     */
}
     /*
     * 因为 test0 变量超出其作用域,强引用失效。
     * 所以自动释放 Test 对象 A。
     * 
     * 因为 test1 变量超出其作用域,强引用失效。
     * 所以自动释放 Test 对象 B。
     * 
     * 此时,持有Test对象 A 的强引用的变量为
     * Test 对象 B 的obj_。
     * 
     * 此时,持有 Test 对象 B 的强引用的变量为
     * Test 对象 A 的obj_。
     * 
     * 发生内存泄漏!
     */

循环引用容易发生内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。
像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)。

    id test = [[Test alloc] init];
    [test setObject:test];

__weak 修饰符与 __strong 修饰符相反,提供弱引用。弱引用不能持有对象实例。

{
    /*
     * 自己生成并持有对象
     */ 
      id ___strong obj0 = [[NSObject alloc] init];
    /*
     * 因为变量 obj0 为强引用,
     * 所以自己持有对象
     */ 

      id __weak obj1 = obj0;
   /*
     * 因为变量 obj0 为强引用,
     * 所以自己持有对象
     */ 
}
    /*
     * 因为变量 obj0 超出其作用域,强引用失效,
     * 所以自动地释放自己持有的对象。
     * 因为对象的所有者不存在,所以废弃该对象。
     */ 

__weak 修饰符还有另一有点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被复制的状态(空弱应用)。如以下代码所示。

id __weak obj1 = nil;
{
    id __strong obj0 = [[NSObject alloc] init];
    obj1 = obj0;
    NSLog(@"A: %@", obj1);
}
NSLog(@"B: %@", obj1);

此源代码执行结果如下:

A: <NSObject: 0x753e180>
B: (null)

遗憾的是,__weak 修饰符只能用于iOS5以上及OS X Lion以上版本的应用程序。在iOS4以及OS X Snow Leopard的应用程序中使用__unsafe_unretained 修饰符来代替。

__unsafe_unretained 修饰符

__unsafe_unretained 修饰符正如其名unsafe所示,是不安全的所有权修饰符。

  • __unsafe_unretained: 不会对对象进行retain,当对象销毁时,会依然指向之前的内存空间(野指针)
  • __weak: 不会对对象进行retain,当对象销毁时,会自动指向nil
已经有了__weak 为什么还要保留 __unsafe_unretained ?
__autoreleasing 修饰符

ARC有效时不能使用autorelease方法,也不能使用NSAutoreleasePool类。虽然autorelease无法直接使用,但实际上,ARC有效时 autolease功能是起作用的。

    /* ARC 无效 */
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    [pool darin];// 此处等同于 “[obj release]”

ARC有效时,钙源代码也能写成下面这样,

    @autoreleasepool {
        id __autoreleasing obj = [[NSObject alloc] init];          
    }

指定“@autoreleasepool块”来代替“NSAutoreleasePool类对象生成、持有以及废弃”这一范围。
另外,ARC有效时,要通过将对象赋值给附加了 __autoreleasing修饰符的变量来代替调用autorelease方法。对象赋值给附有__autoreleasing修饰符的变量等价于在ARC无效时调用对象的autorelease方法,即对象被注册到autoreleasepool。

但是,显示地附加 __autoreleasing修饰符同显示地附加__strong修饰符一样罕见。
编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到autoeleasepool。

规则

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

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

遵守内存管理的方法命名规则。

在ARC无效时,用于对象生成/持有的方法必须遵守以下的命名规则。

  • alloc
  • new
  • copy
  • mutableCopy

以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC有效时也一样,返回的对象完全没有改变。只是在ARC有效时要追加一条命名规则。

  • init

以 init 开始的方法的规则要比 alloc/new/copy/mutableCopy 更严格。该方法必须是实例方法,并且必须要返回对象。返回的对象应为id类型或该方法声明类的对象类型,抑或是该类的超类型或子类型。

属性

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

@property (nonatomic, strong) NSString *name;

当ARC有效时,以下可作为这种属性声明中使用的属性来用。如表1-3所示。

表1-3 属性声明的属性与所有权修饰符的对应关系
属性声明的属性 所有权修饰符
assign __unsafe_unretained 修饰符
copy __strong 修饰符(但是赋值的是被复制的对象)
retain __strong 修饰符
strong __strong 修饰符
unsafe_unretained __unsafe_unretained 修饰符
weak __weak 修饰符

assign与weak,它们都是弱引用声明类型,最大的区别在那呢?
如果用weak声明的变量在栈中就会自动清空,赋值为nil。
如果用assign声明的变量在栈中可能不会自动赋值为nil,就会造成野指针错误!

  • 修饰变量类型的区别
    weak 只可以修饰对象。如果修饰基本数据类型,编译器会报错-“Property with ‘weak’ attribute must be of object type”。
    assign 可修饰对象,和基本数据类型。当需要修饰对象类型时,MRC时代使用unsafe_unretained。当然,unsafe_unretained也可能产生野指针,所以它名字是"unsafe_”。
  • 是否产生野指针的区别
    weak 不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃。 weak是安全的。
    assign 如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。
以上部分内容是摘自Kazuki Sakamoto Tomohiko Furumoto 著的《Objective-C高级编程 iOS与OS X多线程和内存管理》一书中的第一章“自动引用计数”。有兴趣深入了解的朋友可以看看。
上一篇下一篇

猜你喜欢

热点阅读