IOS 中的内存管理技术

前言

内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和C++,您必须进行内存管理。本文将介绍手工的、自动的内存管理实践的基本概念。


一、属性修饰符

说到内存管理,不可避免的要提到属性修饰符。在本章中我们只关心跟内存管理相关的修饰符:assign、copy、weak、retain、strong。assign 主要是用来修饰基本数据类型;copy 可以用来修饰NSString 或者 对象类型;weak 修饰 对象类型,避免出现强强引用的情况;retain 用来修饰对象类型,使得引用计数+1,避免内存释放(MRC中的概念);strong 修饰 对象类型,与retain效果一样(ARC中新引入的概念)。下面将结合代码一一的说明这5个修饰符的用法,以及什么时候该用哪个修饰符。


二、assign修饰符介绍

assign 用来修饰基本数据类型,比如NSInteger、BOOL、int等数据类型,其实这个修饰符放到内存管理里面来介绍 不是很合理,因为assign并不参与内存管理(在MRC下的代码与ARC下的代码完全一样)。代码如下:

@property (nonatomic, assign) BOOL stop;

@property (nonatomic, assign) NSInteger count;

场景:如果程序中 你定义了一个Person类,并且在另外一个类World 的属性定义中使用了assign.

我们先看一下在MRC下的代码书写 以及 结果:

World class attribute declear

@property (nonatomic, assign) Person *person;

World class example method implement

1、self.person = [[Person alloc] init];

在上面1标识的语句结束之后,如果World类中还有使用self.person的地方,还是可以使用的。因为自己生成的对象自己持有,这时候self.person.retainCount 的值为1

NSLog(@"%ld", self.person.retainCount);

ARC环境下代码结果:

编译器会提示warning:

**assigning retained object to unsafe property; object will be released after assignment**

提示你,说assign修饰的object 为不安全的属性,对象将会被释放,当赋值语句结束后,后面如果再想使用self.person 的属性或者方法,程序则会crash。 代码执行完毕之后,如果在World类的其他方法中调用self.person.name,看一下效果。这时候调用self.person.name 因为self.person 指向的内存地址被其他的对象占用了,造成无法预知的错误。有可能内存没有被其他对象使用,因为内存地址中已经没有任何Person的信息了,造成的错误提示为:

Thread-1:Bad-Exe-Access;

如果这部分内存被其他的对象占用了,就会直接Crash,并且输出crash日志,例如:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSPathStore2 name]: unrecognized selector sent to instance 0x7fbe58f90db0'

所以,为了避免不可预知的错误,在修饰对象的时候一定不要使用assign


三、copy修饰符介绍

copy修饰NSString

目前通常用在NSString *对象上面,当然也可以用在自定义对象以及Foundation框架中的其他对象上面,copy 的在ios 中的语义表示 重新copy 出来一份内存,并保持;下面我们通过代码的形式解读copy的用法。

假如我们定义了类Person,我们可以这样使用copy类修饰对象:

@property (nonatomic, copy) NSString *name;

有如下代码给Person 对象的name属性赋值

NSString *name = @"terry";

self.name = name;

那么,这时候如果你比较两个变量的地址的时候会发现,这两个变量的地址并指向同一块内存地址。当然可以尝试一下

NSLog(@"%d", self.name == name);

使用 == 表示对于变量地址的比较,这时候你会看到控制台打印一个:1。

但是Copy这个修饰符 在不同的场景下,表现出来的行为是不一样的。后面我会再写一篇文章介绍关于NSString 的有趣的一些语法 与 表现差异。

copy修饰其他对象

在这里为了更好的说明修饰其他对象,我将Person代码实现出来。

@interface Person : NSObject<NSCopying> 
@property (nonatomic, copy) NSString *name;  

- (instancetype)initWithName:(NSString *)name;
@end

@implement Person 
- (instancetype)initWithName:(NSString *)name {
    self = [super init];
    if(self) {
        self.name = name;
    }
    return self;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    //这边代码的实现 决定了Copy 修饰符用在对象的区别
    //第一种实现:
    return self;
    //第二种实现:
    Person *person = [[Person alloc] initWithName:@"terry"];
    return person;
}
@end

还是World类中有Person的一个引用

World class declear

@property (nonatomic, copy) Person *person;

World class example method

self.person = [[Person alloc] initWithName:@"terry"];

如果Person中的copyWithZone 是第一种实现,这时候,你就会发现self.person的引用计数目前已经是2了。因为Person的copy方法返回的还是自己。如果这种代码出现在MRC中的话,就会造成内存泄露。在MRC中,给self.person的正确赋值方式应嘎是下面的情况

self.person = [[[Person alloc] initWithName:@"terry"] autorelease];

这样在代码段结束之后,即AutoReleasing代码块结束之后,就会释放,将self.person指向的对象的引用计数-1,从而达到内存管理,避免内存泄露的问题。当然,MRC中的代码也可以这样写

Person *person = [[Person alloc] initWithName:@"terry"]; self.person = person; [person release];

与上面这段MRC的代码对应的ARC的代码应该是

Person *person = [[Person alloc] initWithName:@"terry"]; self.person = person;

其实通过上面的ARC 与 MRC的代码对比我们也可以看到,LLVM将ARC代码转化成MRC代码的实现原理,使得开发者不需要关心内存管理的问题。

紧跟着上面的实现,World中有Person的引用,而Person中也有World的引用,来表示这个人属于哪个世界,如果这两个对象都是用copy修饰,并且都实现了copyWithZone方法,方法内都开辟出来一块新内存,则并不会存在内存泄露的情况,如果这两个copyWithZone对象存在浅复制的情况则会造成内存泄露。用代码验证一下:

- (instancetype)copyWithZone:(NSZone *)zone {
    Person *person = [[Person alloc] init];
    person.name = @"terry";//1
    person.world = [self.world copy];
    return person;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    World *world = [[World alloc] init];
    world.person = [self.person copy];//2
    return world;
}

//测试代码
world = [[World alloc] init];
person = [[Person alloc] init];
person.name = @"terry";

world.person =person;
person.world = world;

如果是上面的实现,因为每一个对象都是新new出来的,那么不会存在内存泄露,但是如果代码中的1、2注释的那两行去掉,那么就表示了对象的浅复制,一定有一个对象引用了自己,造成内存泄露。所以,为了解决这种问题,IOS 中引入了weak的概念。

四、weak 与 strong(retain) 修饰符的使用

weak主要是用来解决两个对象之间相互引用造成的两个对象的内存无法释放的问题。上面说的那种情况

@interface World : NSObject
    @property (nonatomic, copy) Person *person;
@end

@interface Person : NSObject
    @property (nonatomic, strong) NSString *name;

    @property (nonatomic, strong) World *world;
@end

将修饰符全部改为strong,并且删除copyWithZone方法,这样的引用方式必然会造成内存泄露,因为两个对象的引用计数都为2+,无法释放。如果将其中的一个对象的属性修饰符改成weak则不会存在强强引用的情况。因为weak修饰之后 引用计数并不会+1,如下代码将world修饰符改为weak

@interface Person : NSObject
    @property (nonatomic, strong) NSString *name;

    @property (nonatomic, weak) World *world;
@end

那么,使用如下测试代码:

world = [[World alloc] init];
person = [[Person alloc] init];
person.name = @"terry";

world.person =person;
person.world = world;

代码执行完毕之后,person引用计数为2,world的引用计数为1.但是person会在autoreleasing之后释放;所以,就只有world.person保持person对象,引用计数减为1.person生命周期结束之后,内存会随之释放。

而这时候strong修饰符的使用也就没什么好说的了,被strong 修饰的对象,引用计数+1.

五、在写博客过程中发现的问题

类函数与成员函数的区别

第一点:写法不同,这种很明显的区别显而易见。

第二点:内存管理方面的区别,比如

NSArray *array = [NSArray array];

NSArray *array = [[NSArray alloc] init];

记得前面也有提到过MRC 与ARC的转换,下面看这段代码就了解了类函数 与 成员函数的区别了。在MRC中需要这么写如下代码,否者会造成内存泄露

NSArray *array = [[[NSArray alloc] init] autoreleasing];

也可以这样写:

NSArray *array = [[NSArray alloc] init]; 
/*其他代码*/
[array release];

但是如果使用Foundation中的类方法创建对象的话,则不需要上面那样写

NSArray *array = [NSArray array];

这就是两者之间的区别。自己细细的领悟哦。

后序

这是第一次在GitBook上面写博客,记录一下关于内存管理的自己的一些认知,如果有什么不对的,欢迎批评指正,大家共同进步。后续也会增加对于IOS 各方面的研究博客。欢迎 关注。