欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS内存管理

程序员文章站 2022-07-10 12:10:40
在学习内存管理的时候,查阅了不少资料,零零散散的记录在有道云笔记中,在这里总结提炼一下,希望在方便自己查看的同时能帮助到大家。 1.引用计数 在引用计数架构下,每个对象都有个可...

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

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,来考虑引用计数式内存管理的思考方式。

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

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,并取得非自己生成并持有的对象。