iOS内存管理

iOS内存管理

主要参考资料:《Effective Objective-C 2.0》,《Objective-C高级编程 iOS与OS X多线程和内存管理》

在学习内存管理的时候,查阅了不少资料,零零散散的记录在有道云笔记中,在这里总结提炼一下,希望在方便自己查看的同时能帮助到大家。

1.引用计数

在引用计数架构下,每个对象都有个可以递增或递减的计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在OC中叫做“保留计数(retain count)”,不过也可以叫做“引用计数”。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其引用计数。计数变为0,就表示没人关注此对象了,于是就可以把它销毁。

MRC:在手动引用计数模式下,NSObject协议声明了下面三个方法用于操作计数器,retain,release,autorelease。
- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;

ARC:苹果的官方说明:
在OC中采用ARC机制,让编译器来进行内存管理。在新一代Apple LLVM编译器中设置ARC为有效状态,就无需再次键入retain或release代码,这在降低程序崩溃、内存泄露等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并能立即释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅提升。

虽然现在项目中推荐使用ARC,但是理解MRC是非常有用的。

2. 引用计数式内存管理的思考方式

让我们先忽略ARC,来考虑引用计数式内存管理的思考方式。

  1. 自己生成的对象,自己所持有
  2. 非自己生成的对象,自己也能持有
  3. 不再需要自己持有的对象时释放该对象
  4. 非自己持有的对象无法释放

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

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

  • alloc
  • new
  • copy
  • mutableCopy

在OC中方法名能体现出内存管理语义,以上面这4个名称开头的方法名表示“生成的对象自己持有”,也可以说成“生成的对象归调用者所有”。“生成的对象自己持有”意味着:调用上述四种方法的那段代码要负责释放方法所返回的对象。

{
    //调用[[NSObject alloc]init]方法会在堆上产生一个对象,并且没有被release和autorelease。这个堆上的对象目前的引用计数为1
    //把这个对象的指针赋值给obj。赋值不会导致引用计数的变化,相当于通过obj可以拿到这个堆上的对象使用了。
    id obj = [[NSObject alloc]init];

    //这个代码段快要结束了,把这个堆上的对象释放掉,不然会产生内存泄漏。
    [obj release];  
}

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

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

//取得非自己生产并持有的对象
//调用[NSMutableArray array]方法,会在堆上产生一个对象,这个对象被调用了autorelease方法,它会再下一次运行循环的时候被release。可以把这个对象想象成引用计数为0的对象(虽然是过一段时间才变为0),只有retain一下,才能持有这个对象。
id obj = [NSMutableArray array];
//取得的对象存在,但自己不持有对象
//NSMutableArray对象被赋给变量obj,但变量obj自己并不持有该对象。使用retain方法可以持有对象。
//取得非自己生产并持有的对象
id obj = [NSMutableArray array];
//取得的对象存在,但自己不持有对象
[obj retain];
//自己持有对象

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

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

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

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

如此,用alloc方法由自己生成并持有的对象就通过release方法释放了。
用alloc/new/copy/mutableCopy方法生产并持有的对象,或者用retain方法持有的对象,一旦不再需要,务必要用release方法进行释放。

如果要用某个方法生成对象,并将其返还给该方法的调用方,那么它的源代码又是怎样的呢?

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

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

allocObject名称符合前文的命名规则,因此它与用alloc方法生成并持有的对象的情况完全相同,所以使用allocObject方法也就意味着“自己生成并持有对象”。

那么,调用[NSMutableArray array]方法使取得的对象存在,但自己不持有对象,又是如何实现的呢?根据命名规则,不能使用以alloc/new/copy/mutableCopy开头的方法名,因此使用object这个方法名。

-(id)object
{
    id obj = [[NSObject alloc]init];
    //自己持有对象
    [obj autorelease];
    //取得的对象存在,但自己不持有对象
    return obj;
}

上例中我们使用了autorelease方法。使用该方法,可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放(调用release方法)。
autorelease是把对象注册到自动释放池,等pool结束时自动调用release方法。

使用NSMutableArray类的array类方法等可以取得谁都不持有的对象,这些方法都是通过autorelease而实现的。此外,根据命名规则,这些用来取得谁都不持有的对象的方法名不能以alloc/new/copy/mutableCopy开头,这点需要注意。

id obj1 = [obj0 object];
//取得的对象存在,但自己不持有
//当然,也能通过retain方法将调用autorelease方法取得的对象变为自己持有。
id obj1 = [obj0 object];
//取得的对象存在,但自己不持有
[obj1 retain];
//通过retain使得自己持有对象

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

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

//自己生成并持有对象
id obj = [[NSObject alloc]init];
//自己持有对象
[obj release];
//对象已释放
[obj release];
//释放之后再次释放已非自己持有的对象,应用崩溃

或者在“取得的对象存在,但自己不持有对象”时释放

id obj1 = [obj0 object];
//取得的对象存在,但自己不持有对象
[obj1 release];
//释放了非自己持有的对象,这肯定会导致应用程序崩溃

帮助理解:
使用与持有是两回事。你可以使用这个对象,但是你并不是它的所有者。比如有个对象的所有者已经对它进行了autorelease,你可以使用这个对象,但是除非你进行了retain,否则你不是它的所有者。
从所有权的角度考虑内存管理问题,就能很容易解释为什么需要在description方法中向返回的NSString对象发送autorelease消息:
因为该只是创建了一个NSString对象,但是并不想拥有该对象。创建NSString的对象只是为了返回一个结果,将NSString对象“交出去”而已。交出去后,别人爱拥有就拥有,不爱拥有就算了。

3. ARC

实际上“引用计数式类存管理”的本质部分在ARC中并没有改变。就像“自动引用计数”这个名称表示的那样,ARC只是自动地帮助我们处理“引用计数”的相关部分。在编译单位上,可设置ARC有效或无效,这一点便能佐证上述结论。比如对每个文件可选择使用或不使用ARC。

上述的内存管理思考方式在ARC中依然有效,只是在源代码的的书写上有些不同。到底有什么样的变化呢?首先要理解ARC中追加的所有权声明。

3.1所有权修饰符

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

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

3.1.1 __strong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符。

id obj = [[NSObject alloc]init];
该obj变量实际上被附加了__strong所有权。相当于:

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

再看如下的代码段:

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

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

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

如此源代码所示,附有__strong修饰符的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。
如strong这个名称所示,__strong修饰符表示对对象的“强引用”。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

3.1.2 __weak修饰符
有些时候会出现循环引用的问题:

{
        Dog* dog1 = [[Dog alloc]init];//狗对象;dog1持有狗对象的强引用
        Cat* cat1 = [[Cat alloc]init];//猫对象;cat1持有猫对象的强引用
        [dog1 setObj:cat1];//狗对象里的成员变量obj_持有猫对象的强引用
        [cat1 setObj:dog1];//猫对象里的成员变量obj_持有狗对象的强引用
        //现在狗对象的持有者是dog1、猫对象的成员变量obj
        //现在猫对象的持有者是cat1、狗对象的成员变量obj
    }
    //因为dog1,cat1超出作用域,强引用失效,所以自动释放狗对象和猫对象,引用计数从2变为1
    //此时两个对象中的成员互相引用两个对象,发生内存泄露

像下面这种情况,虽然只有一个对象,但在该对象持有其自身时,也会发生循环引用(自引用)。

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

使用__weak修饰符可以避免循环引用。弱引用不能持有对象,对对象的引用计数没有影响。还是那句话,可以使用但不持有,持有者把它release了,你就不能使用了。把之前的成员变量的修饰符改成__weak即可解决循环引用问题。
__weak修饰符还有另一个优点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且被赋值为nil。通过检查附有__weak修饰符的变量是否为nil,可以判断被赋值的对象是否已废弃。

3.1.3 __unsafe_unretained修饰符
附有_unsafe_unretained修饰符的变量同附有__weak修饰符的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即被释放。到这里,__unsafe_unretained修饰符和__weak修饰符是一样的。

__unsafe_unretained修饰的变量与__weak有点区别是:在对象被废弃时,不会被赋值为nil。

3.1.4 __autorelease修饰符
用附有__autoreleasing修饰符的变量替代autorelease方法。
前面说到,用alloc/new/copy/mutableCopy以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的所有者。
我们自己编写不以这些开头的方法时,编译器会按照规则帮我们在要返回的对象上调用autorelease方法。
有种情况:
比如NSData中的一个方法:

- (BOOL)writeToFile:(NSString *)path options:(NSDataWritingOptions)writeOptionsMask error:(NSError **)errorPtr;

我们需要声明一个NSError *error;然后把&error传入方法中。
这里的(NSError **)errorPtr,其实等同于

(NSError *  __autoreleasing *)errorPtr

这样就会把*error加入到自动释放池中。之所以这样做,是为了符合内存管理的思考方式,作为alloc/new/copy/mutableCopy方法返回值取得的对象是自己生成并持有的,其他情况下便是取得非自己生成的对象。因此,使用附有__autoreleasing修饰符的变量作为对象取得参数,与除alloc/new/copy/mutableCopy外其他方法的返回值取得的对象完全一样,都会注册到autoreleasepool,并取得非自己生成并持有的对象。

你可能感兴趣的:(ios编程)