应用程序内存管理是在程序运行时分配内存(比如创建一个对象,会增加内存占用)与清除内存(比如销毁一个对象,会减少内存占用)的过程
目前iPhone手机内存大多为1G,分配给每个应用程序的内存空间极其有限,当应用程序运行过程中所占用的内存较大时,便会收到系统给出的内存警告,如果应用程序所占用的内存超过限制时,便会被系统强制关闭,所以我们需要对应用程序进行内存管理,一个好的程序程序也应该尽可能少地占用内存
在OC中提供了两种管理内存的方式
注: 在Build Setting中的Objective-C Automatic Reference Counting设置为YES即为ARC
任何继承自NSObject的对象都需要管理内存(因存放在堆里面),基本数据类型int、float、double、char、结构体struct以及枚举enum都不需要管理内存(因存放在栈里面)
内存管理模型基于对象所有权机制,一个对象的所有权可以拥有一个或者多个拥有者,只要一个对象的所有权拥有至少一个拥有者,该对象就会一直存在于内存中.如果一个对象的所有权没有任何拥有者,那么系统便会自动地将该对象所占用的内存释放掉.为了明确何时拥有某个对象的所有权、何时放弃某个对象的所有权,系统设置了如下规则
在iOS5以前,程序员普遍使用MRR&MRC方式来管理内存,程序员需要自己添加retain、release和autorelease等内存管理代码来跟踪自己所拥有的对象以明确地管理对象的内存,在需要使用该对象的时候保证该对象一直存在于内存中,在不需要使用该对象的时候保证该对象所占用的内存被系统正常回收
为了让系统知道何时需要将某个对象所占用的内存清理掉,系统引入了引用计数器的概念
系统为每个OC对象内部都分配了4个字节的存储空间存放自己的引用计数器,引用计数器是一个整数,表示“对象被引用的次数”,当对象的引用计数器为0时,对象所占用的内存空间就会被系统自动回收,当对象的引用计数器不为0时,在程序运行过程中所占用的内存会一直存在,直到整个程序退出时由OS自动释放
注: 给对象发送一条release消息,不代表对象会被释放,只有对象的引用计数器为0时才会被释放
下面通过一个小示例,来演示一下如何操作对象的引用计数器
Person *p = [[Person alloc] init]; // 使用alloc创建一个新对象,对象引用计数器 = 1
[p retain]; // 给对象发送一条retain消息,对象引用计数器 + 1 = 2
[p release]; // 给对象发送一条release消息,对象引用计数器 - 1 = 1
[p release]; // 给对象发送一条release消息,对象引用计数器 - 1 = 0,指针所指向的对象的内存被释放
下面介绍几个关于内存管理方面常常提到的术语
注: 默认情况下,Xcode是不会监听僵尸对象的,所以需要我们自己手动开启,开启监听僵尸对象步骤为: Edit Scheme ->; Run ->; Diagnostics ->; Objective-C的Enable Zombie Objects打钩,这样便可以在因为僵尸对象报错的时候给出更多错误信息
Person *p = [[Person alloc] init]; // 引用计数器 = 1
[p release]; // 引用计数器 - 1 = 0,指针所指向的对象的内存被释放
[p release]; // 这句给野指针发送消息,会报野指针错误,开启监听僵尸对象会给出错误信息**- -[Person release]: message sent to deallocated instance 0x100206fd0
注: 如果在第一次给对象发送release消息后,立刻将指针置空,便不会出现野指针错误,因为给空指针发送消息不会报错,系统什么也不会做,所以在对象被释放时将指针设置为nil可以避免野指针错误
单对象的内存管理比较简单,当使用alloc、new、copy、mutableCopy创建一个新对象时,该新对象的引用计数器为1,我们只需要在对象不再使用的时候给其发送一次release消息,让对象的引用计数器-1变为0即可正常释放
下面通过一个小示例,来演示一下如何对单对象进行内存管理
// 在堆中开辟一块内存存放Person对象,在栈中开辟一块内存存储局部变量p,p中存放的是Person对象的地址
// 当出了大括号,局部变量p出了作用域便会被系统自动回收,而Person对象引用计数器为1仍然存在,这样便造成内存的泄漏,所以我们需要在局部变量p被系统回收之前,给p所指向的Person对象发送一条release消息让对象引用计数器-1变为0
Person *p = [[Person alloc] init];
[p release];
相对于单对象内存管理而言,多对象内存管理显得比较复杂,但是只要我们遵从基本的内存管理规则,即可避免大部分内存管理相关的问题
在涉及到多对象内存管理时,平时我们使用的存取器方法便需要针对内存管理进行一些调整
// 针对基本数据类型
- (int)age
{
return _age;
}
// 针对OC对象
- (Room *)room
{
return _room;
}
// 针对基本数据类型,直接将新值赋值给实例变量
- (void)setAge:(int)age
{
_age = age;
}
// 针对OC对象,方式一: 判断两次传入的对象是否相同,如果不同便release旧对象retain新对象,并将新对象赋值给实例变量
- (void)setRoom:(Room *)room
{
if (_room != room)
{
[_room release];
_room = [room retain];
}
}
// 针对OC对象,方式二: retain新对象,release旧对象,并将新对象赋值给实例变量
- (void)setRoom:(Room *)room
{
[room retain];
[_room release];
_room = room;
}
注1: release旧对象retain新对象原因: 避免设置两次不同的room,在Person对象释放时,第一次设置的对象无法被释放,造成内存泄露
注2: 判断当前对象与传入对象是否相同原因: 避免设置两次相同的room,在设置第二次时,将room已经释放了,再调用retain造成野指针错误
当系统回收对象的内存时,系统会自动给该对象发送一条dealloc消息,我们一般会重写dealloc方法,在这里给当前对象所拥有的资源(包括实例变量)发送一条release消息(基本数据类型不用),保证自身所拥有的资源也可以正常释放(因为在使用该资源的时候,采用retain获取了该资源的所有权,在自身释放的同时,也应该放弃对该资源的所有权)
- (void)dealloc
{
NSLog(@"Person dealloc");
// release对象所拥有资源
[_room release];
// 设置为nil可以避免野指针错误(其实可以不设置,只是写了显得有逼格)
_room = nil;
[super dealloc];
}
注1: 不要直接调用对象的dealloc方法
注2: 重写dealloc方法时,一定要调用[super dealloc]方法,且放在代码的最后
注3: 当应用程序被关掉,dealloc方法不一定会被调用,因为由系统OS直接来释放内存比调用dealloc释放内存效率得多
注4: 不要在dealloc方法中管理稀缺资源(比如网络连接,文件资源,DOS命令等),因为dealloc并不一定都是立即调用,有可能会延迟调用,也可能根本不会被调用
属性可以帮助我们免除重复地写存取器代码的麻烦,并且合理使用属性修饰符也可以帮助我们生成符合内存管理规范的代码
在Xcode4.4之前
@property double weight;
- (void)setWeight:(double)weight;
- (double)weight;
@synthesize weight = _weight;
- (void)setWeight:(double)weight
{
_weight = weight;
}
- (double)weight
{
return _weight;
}
注1: 在@synthesize后面告诉编译器,将要实现哪个@property的实现,在等号后边告诉编译器,将setter传进来的值赋值给谁和getter返回谁的值给调用者
注2: 在@synthesize中如果不写等号后面的,系统会默认将setter传进来的值赋值给与@synthesize后面的变量同名的成员变量,且getter返回与@synthesize后面的变量同名的成员变量的值给调用者,即weight而不是_weight
在Xcode4.4之后
注: 不用写@synthesize了,系统会默认为将setter传进来的值赋值以下划线开头的成员变量和getter返回以下划线开头的成员变量给调用者
注: 使用@property并不会帮忙实现dealloc中的[_name release];所以仍需要自己释放对象所拥有的资源
属性修饰符用来修饰@property,给@property添加特定功能,以NSNumber对象为例
@property NSNumber *number;
- (void)setNumber:(NSNumber *)number
{
_number = number;
}
@property (retain) NSNumber *number;
- (void)setNumber:(NSNumber *)number
{
if (_number != number)
{
[_number release];
_number = [number retain];
}
}
在OC中有如下几类属性修饰符
retain, assign, copy
readonly, readwrite
atomic, nonatomic
setter, getter
// 枚举
typedef NS_ENUM(NSInteger, Sex)
{
SexMan,
SexWoman
};
// 结构体
typedef struct
{
int year;
int month;
int day;
} Date;
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) Sex sex;
@property (nonatomic, assign) Date birthday;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain, readonly) NSString *phone;
@property (nonatomic, assign, getter = isRich) BOOL rich;
@end
@implementation Person
- (void)dealloc
{
NSLog(@"Person dealloc");
[_name release];
[_phone release];
[super dealloc];
}
@end
@property (nonatomic, retain) NSNumber *count;
- (void)reset
{
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release];
}
// 虽然下述方法也能实现,但是这种方式避开了存取器方法,可能会导致一些错误(比如实例变量内存管理规则发生了变化,并且这种方法不符合KVO规则)
- (void)reset
{
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[_count release];
_count = zero;
}
- init
{
self = [super init];
if (self)
{
_count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}
- initWithCount:(NSNumber *)startingCount
{
self = [super init];
if (self)
{
_count = [startingCount copy];
}
return self;
}
- (void)dealloc
{
[_count release];
[super dealloc];
}
import是一个预编译指令,他会将”“中的文件拷贝到import所在的位置,不过一旦”“中的文件发生变化,那么import就会重新拷贝一次,从而降低编译效率
在开发中经常会出现循环import的情况,表现形式为在A.h文件中import了B.h文件,在B.h文件中import了A.h文件,那么A.h和B.h两个文件会相互拷贝,造成循环import报错
@class ClassName;仅仅是告诉编译器,ClassName是一个类,不会做任何拷贝操作,不过由于@class仅仅是告诉编译器ClassName是一个类,并不知道ClassName类中到底有什么方法,所以在.m文件中使用该类时还是要#import该类
在开发中我们可以使用@class来避免循环import的问题,即在想要使用一个类的时候,可以在.h中使用@class ClassName;来声明一个类,真正使用这个类的时候在.m中#import “ClassName”即可
注: 在一个ClassName发生变化时,一般只有import了它的.m类才需要重新拷贝,并不会让其他类间接受到影响(因为其他类不会引用.m文件)
如果A对象要拥有B对象的所有权,同时B对象也要拥有A对象的所有权,那么就会产生循环引用,导致两者都无法被正常释放
@interface Person : NSObject
@property (nonatomic, retain) Car *car;
@end
@interface Car : NSObject
@property (nonatomic, retain) Person *person;
@end
针对循环引用的问题,Cocoa建立了一种规则,就是在出现循环引用的情形时,不要让A对象和B对象互相拥有对方的所有权,而是让一方(parent)拥有另一方(children)的”强”引用,另一方(children)拥有这一方(parent)的”弱”引用,即将A retain B, B retain A中一方的retain改为assign即可
@interface Person : NSObject
@property (nonatomic, retain) Car *car;
@end
@interface Car : NSObject
@property (nonatomic, assign) Person *person;
@end
注: 使用assign之后,dealloc中无需再释放该资源
在Cocoa中使用到”弱”引用的例子包括但不局限于table data sources, outline view items, notification observers, and miscellaneous targets and delegates,在处理”弱”引用的对象时一定要小心处理,因为parent并不拥有children的所有权,也就是并不能保证在使用children的时候其一定存在(可能已经被释放),所以在parent自身释放时一定要告知children一下自身已经释放,不要再继续给自己发消息,否则会造成应用程序的闪退(野指针错误).处理办法针对通过中心/观察者而言,便是移除观察者;针对delegate而言,便是setDelegate:为nil,这个操作一般都是在dealloc中进行
自动释放池提供了延迟放弃一个对象的所有权的机制,比如想要在一个方法中返回一个对象,如果先使用release放弃了该对象的所有权,那么return返回的对象便是一个僵尸对象,如果先进行return返回,那么便无法放弃该对象的所有权,导致了内存泄漏
iOS5.0之前创建自动释放池方法(现在也可使用)
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// do something...
[pool release];
iOS5.0之后创建自动释放池方法
@autoreleasepool
{
// do something...
}
autorelease是一种支持引用计数的内存管理方式,只要在自动释放池中给对象发送一条autorelease消息,就会将对象放到自动释放池中,当自动释放池被销毁时,会对池中的所有对象发送一条release消息
注1: 自动释放池被销毁时,只是给池中所有对象发送一条release消息,不代表对象一定会被释放
注2: 对象在自动释放池中每收到一条autorelease消息,在自动释放池被销毁时,对象都会收到一次release消息
Cocoa希望代码在自动释放池中执行,否则发送了autorelease消息的对象便不能收到release消息,从而导致内存泄漏.UIKit库会在自动释放池中处理每一个事件循环,所以我们并不需要显式地创建自动释放池,不过如下三种情况下,我们仍然需要创建自己的自动释放池
for (int i = 0; i < 999999; i++)
{
@autoreleasepool
{
Person *p = [[[Person alloc] init] autorelease];
// do something...
}
}
在开发中,我们经常使用Foundation框架中的类,在调用其类工厂方法创建一个对象时,因为并不是使用alloc,new,copy或者mutableCopy方法创建的,所以并不需要我们自己在给该对象发送release或者autorelease消息,这是因为类工厂方法内部都已经在返回对象前进行过延迟释放
我们在自己书写类工厂方法时,也应该与系统处理方式相同,快速返回一个autorelease对象的方式具体如下
+ (instancetype)person
{
// 使用self而不是使用Person是因为这样可以在子类调用该方法时会返回子类的对象
return [[[self alloc] init] autorelease];
}
快速返回一个带有参数的autorelease对象的方式具体如下
+ (instancetype)personWithName:(NSString *)name
{
Person *person = [[[self alloc] init] autorelease];
person.name = name;
return person;
}
注: Foundation框架中的类,所有通过类工厂方法创建的对象都是autorelease的,所以不用自己进行释放
集合在iOS中包括NSArray、NSDictionary、NSSet,接下来以数组为例讲解一下集合中的内存管理规则
NSMutableArray *array = [NSMutableArray array];
for (NSUInteger i = 0; i < 10; i++)
{
NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
// allocedNumber对象被加入数组,数组会对该对象进行一次retain操作
[array addObject:allocedNumber];
[allocedNumber release];
}
// 当array对象被释放,系统会给数组中的每个allocedNumber对象发送一条release消息,保证数组中的每个元素都能正常释放
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject被从数组中移除,会收到一条release消息,此时heisenObject所指向的对象已经被释放了
// 在实际开发中,我们并不希望这样的情况发生,所以需要使用retain拥有该对象的所有权
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// do something...
[heisenObject release];
id parent = <#create a parent object#>;
heisenObject = [parent child];
[parent release];
// heisenObject所指向的对象是通过另一个对象(例子中的parent对象)获得的,另一个对象是该对象的唯一拥有者,如果另一个对象被释放了,此时heisenObject所指向的对象已经被释放了
// 在实际开发中,我们并不希望这样的情况发生,所以需要使用retain拥有该对象的所有权
id parent = <#create a parent object#>;
heisenObject = [[parent child] retain];
[parent release];
// do something...
[heisenObject release];
ARC(Automatic Reference Counting,自动引用计数),是iOS4引入的一项新技术(从iOS5开始支持弱引用),其使用与MRR&MRC相同的内存管理规则来管理内存,不过编译器会在编译阶段自动地在适当的位置插入retain、release和autorelease等内存管理代码来管理内存(属于编译器特性,不是运行时特性),不再需要程序人员手动管理.官方强烈建议使用ARC方式来管理内存
注: OC中的ARC和Java中的垃圾回收机制不一样,Java中的垃圾回收是系统做的,而OC中的ARC是编译器做的
// MRC
@interface Person : NSObject
@property (retain) NSNumber *number;
@end
@implementation Person
- (void)dealloc
{
NSLog(@"Person dealloc");
[_number release];
[super dealloc];
}
@end
Person *person = [[Person alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2];
person.number = number;
[number release];
[person release];
// perosn和number正常被释放
// ARC
@interface Person : NSObject
@property (strong) NSNumber *number;
@end
@implementation Person
- (void)dealloc
{
NSLog(@"Person dealloc");
}
@end
Person *person = [[Person alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2];
person.number = number;
// perosn和number出了作用域正常被释放
消除了手动管理内存的烦恼,不再需要手动调用retain、release和autorelease等方法来管理内存(实际上在ARC模式下已经不能调用出来该方法了),编译器在编译阶段会自动地在适当的位置插入这些代码,这样可以极大概率地避免出现内存问题.同时编译器也会在一些地方执行某些优化,让代码性能更高,官方提供了一个能够体现出来这种优势的插图,如下
为了使ARC能够正常工作,在ARC中引入了一些区别于当前编译模式的新的规则,如果你违反了这些规则,在编译阶段编译器会给出一个警告
// 错误
@property NSString *newTitle;
// 正确
@property (getter=theNewTitle) NSString *newTitle;
在ARC中新增了两个属性修饰符: strong和weak,其中strong是默认修饰符,下面介绍一下这两个属性修饰符与retain和assign的区别
// 下面这句对于strong的示例,与此同义: @property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;
// 下面这句对于weak的示例,与此相似: @property(assign) MyClass *myObject;
// 使用assign修饰的指针所指向的对象如果被释放,该指针会变成野指针;使用weak修饰的指针所指向的对象如果被释放,该指针会变成空指针
@property(weak) MyClass *myObject;
针对于ARC中属性修饰符的使用,要进行如下变化
注: 其实就是将MRC中的assign分成了两个部分,分别用于修饰OC对象与基本数据类型
在ARC中新增了四个变量修饰符: 双下划线strong、双下划线weak、双下划线unsafe_unretained和双下划线autoreleasing,其中双下划线strong是默认修饰符,下面介绍一下这四个变量修饰符
官方提醒,在为变量添加修饰符时,最正确的方式如下
// 规则
ClassName * qualifier variableName;
// 正确示例
MyClass * __weak myWeakReference;
MyClass * __unsafe_unretained myUnsafeReference;
// 错误示例(虽然错误,但是编译器会默认为正确,官方说法为"forgiven")
__weak MyClass * myWeakReference;
__unsafe_unretained MyClass * myUnsafeReference;
注: 在直接使用__weak修饰变量指向一个刚创建的对象时,需要注意对象刚刚创建出来就会释放的情况
NSString * __weak string = [[NSString alloc] initWithFormat:@"loly"];
// 因为没有强指针指向该对象,该对象会立即被释放
ARC中判断对象是否应该被释放,不再观察引用计数器是否为0,思想应该转变为是否有强指针指向该对象,只要有一个强指针指向该对象,该对象就会一直保持在内存中不会被释放
在涉及到多对象内存管理时,在MRC中使用的存取器方法也需要进行一些调整
// 针对基本数据类型
- (int)age
{
return _age;
}
// 针对OC对象
- (Room *)room
{
return _room;
}
// 针对基本数据类型,直接将新值赋值给实例变量
- (void)setAge:(int)age
{
_age = age;
}
// 针对OC对象,直接将新值赋值给实例变量
- (void)setRoom:(Room *)room
{
_room = room;
}
因为ARC使用与MRC相同的引用计数规则,所以同样也会出现循环引用问题
在MRC中的解决办法是让parent持有children的”强”引用,而children持有parent的”弱”引用,即将A retain B, B retain A中一方的retain改为assign即可
在ARC中的解决办法与在MRC中原理相同,即将A strong B, B strong A中一方的strong改为weak即可
@interface Person : NSObject
@property (nonatomic, strong) Car *car;
@end
@interface Car : NSObject
@property (nonatomic, weak) Person *person;
@end
注: 当使用Block时,会出现不易察觉的循环引用现象,关于这点我在另一篇文章中进行过介绍,这里不再赘述
关于自动释放池在上文已经详细介绍过,这里不再赘述
// MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// do something...
[pool release];
// ARC
@autoreleasepool
{
// do something...
}
在ARC中,栈变量会被初始化为nil,即使不进行赋值,程序也不会造成崩溃,示例如下
- (void)myMethod
{
NSString *name;
NSLog(@"name: %@", name);
// 打印结果为nil,不会崩溃
}
copy(复制、拷贝)是产生一个副本对象的过程,只要是通过拷贝产生的副本对象,副本对象中的内容与源对象中的内容就完全一致,下面介绍几个copy相关的知识点
通过拷贝是否会产生新对象,就要看源对象与副本对象是否满足拷贝的特点
注1: 通过深拷贝,源对象的引用计数器不变,副本对象的引用计数器为1,所以需要对副本对象进行一次release操作
注2: 通过浅拷贝,源对象的引用计数器+1,所以需要对源对象再进行一次release操作
注: 字符串的属性都应该使用copy修饰符进行修饰
@interface Person : NSObject
@property (nonatomic, copy) NSString *theCopyName;
@property (nonatomic, strong) NSString *theStrongName;
@end
Person *p = [[Person alloc] init];
NSMutableString *mStr = [NSMutableString stringWithString:@"loly"];
p.theStrongName = mStr;
p.theCopyName = mStr;
[mStr appendString:@"y"];
NSLog(@"theStrongName = %@, theCopyName = %@", p.theStrongName, p.theCopyName);
// 打印结果: theStrongName = loly, theCopyName = lol
一个类的对象想拥有copy与mutableCopy功能,都需要拥有一个前提
注: OC中大部分类已经遵守了NSCopying,NSMutableCopying协议
下面我们将通过两个示例来介绍一下如何让自定义类拥有copy与mutableCopy功能
@interface Person : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation Person
- (id)copyWithZone:(NSZone *)zone
{
// 1.创建一个新对象
Person *p = [[[self class] allocWithZone:zone] init];
// 2.设置当前对象的内容给新的对象
p.name = _name;
p.age = _age;
// 3.返回新的对象
return p;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
Person *p = [[[self class] allocWithZone:zone] init];
p.name = _name;
p.age = _age;
return p;
}
@end
@interface Student : Person
@property (nonatomic, assign) float height;
@end
@implementation Student
- (id)copyWithZone:(NSZone *)zone
{
// 1.创建一个新对象
id obj = [super copyWithZone:zone];
// 2.设置当前对象的内容给新的对象
[obj setHeight:_height];
// 3.返回新的对象
return obj;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
id obj = [super mutableCopyWithZone:zone];
[obj setHeight:_height];
return obj;
}
@end