IOS内存管理
本文是本人学习ios开发后总结出的一些经验,如有讲得不对或有误的地方,请各位高富帅批评指正。
1. 本文主题是ios内存管理,分为两种方式:手动内存管理与ARC内存管理。
1)手动内存管理:就是在堆上分配一个对象使用完成后,不需要该对象时程序员需要自己记得释放该对象占用的内存。如果程序员忘记释放内存,就会导致该对象一直占用该内存空间,从而发生内存泄漏问题。下文将会详细分析手动内存管理问题。
2)ARC内存管理:ARC就是Automatic reference counting的缩写。苹果公司为IOS app提供了一种由系统自身管理对象释放内存问题,程序员只需要关心创建对象问题,而不需要关心对象释放问题。那么,是否是使用了ARC机制后,IOS app就不会发生内存泄漏与野指针问题呢?下文会作出详细的解释。
2. 内存管理主要解决两个问题:野指针与内存泄漏。
1)野指针:一般理解的野指针概念指的是对象已被释放或指针变量指向的对象已不存在,然后向该对象发出一个SEL消息,导致app异常退出。
2)内存泄漏:在堆上分配一个对象使用完成后,程序员忘记手动释放或是对象之间存在相互strong引用关系,程序已退出,而对象还未被销毁问题。
3. 首先讨论手动内存管理的野指针与内存泄漏问题
1)首先准备两个用户自定义类Person与Car,代码如下:
Person.h内容如下:
#import <Foundation/Foundation.h>
@interface Person : NSObject
{
int _age;
}
- (void) setAge : (int) age;
- (int) age;
@end
Person.m内容如下:
#import "Person.h"
@implementation Person
- (void) setAge : (int) age
{
_age = age;
}
- (int) age
{
return _age;
}
@end
Car.h内容如下:
#import <Foundation/Foundation.h>
@interface Car : NSObject
{
int _speed;
}
- (void) setSpeed : (int) speed;
- (int) speed;
@end
Car.m内容如下:
#import "Car.h"
@implementation Car
- (void) setSpeed : (int) speed
{
_speed = speed;
}
- (int) speed
{
return _speed;
}
@end
2) 下面讨论IOS内存管理
1) 原理:在IOS app中,每一个OC对象都有一个unsigned long类型(假设该变量名称为_referenceCount)变量保存该对象被引用的次数,当_referenceCount == 0时,该对象就会被释放。
2)操作_referenceCount变量相关的方法:
a. 将_referenceCount变量+1的相关方法:alloc、new、copy、retain。
b. 将_referenceCount变量-1的相关方法:release、autorelease。
c. 获取_referenceCount变量的方法:retainCount。
3)内存管理
原则1: 当调用了alloc、new、copy、retain一次,使用完该对象,相应的应该调用release、autorelease一次。即增加方法与减少方法需要配对使用。
如下例子:
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char *argv[])
{
Person *p = [ [Person alloc] init];
//对象的其它操作
......
[p release];
}
从上述的例子可看出,调用了Person类的alloc方法,变量_referenceCount=1,对Person对象进行其它操作之后,调用对象方法release,此时变量_referenceCount=0.系统回收了指针p指向的内存空间。如果在此main函数中,对象使用完成后,没有调用对象的release方法,_referenceCount始终等于1,当main函数退出后,指针变量p被销毁,而p所指向的对象永远不会被释放,所以发生了内存泄漏。
测试验证方法:
在Person中重写dealloc方法,重写代码如下:
-(void) dealloc
{
[super dealloc];
NSLog(@"Person对象被销毁了");
}
如果Person对象被成功的销毁,将会在console输出“Person对象被销毁了”。否则,发生了内存泄漏。
原则2: 当Person对象中包含Car对象的引用时,调用setCar方法时,应该将Car对象的_referenceCount变量加1,在Person的dealloc方法中将Car对象的_referenceCount变量减1.
Person类代码修改如下:
Person.h
#import <Foundation/Foundation.h>
#import "Car.h"
#interface Person : NSObject
{
Car *_car;
int _age;
}
-(void) setCar : (Car *) car;
-(Car *) car;
-(void) setAge : (int) age;
-(int) age;
-(void) driver;
#end
Person.m内容如下:
#import "Person.h"
#implementation Person
- (void) setCar : (Car *) car
{
_car = car;
//_car = [car retain];
}
- (Car *) car
{
return _car;
}
- (void) setAge : (int) age
{
_age = age;
}
- (int) age
{
return _age;
}
-(void) driver
{
[_car run];
}
//重写dealloc方法
-(void) dealloc
{
//[_car release]
[super dealloc];
NSLog(@"Person对象被销毁了");
}
#end
Car.h内容下:
#import <Foundation/Foundation.h>
@interface Car : NSObject
{
int _speed;
}
-(void) setSpeed : (int) speed;
-(int) speed;
-(void) run;
@end
Car.m内容如下:
#import "Car.h"
@implementation Car
-(void) setSpeed : (int) speed
{
_speed = speed;
}
-(int) speed
{
return _speed;
}
-(void) run
{
NSLog(@"Car is running");
}
@end
例子如下:
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
int main(int argc, const char *argv[])
{
Person *p =[ [Person alloc] init];
Car *car = [ [Car alloc] init];
[p setCar : car];
//[p setCar : car];
//对Person对象的其它操作
......
//对Car对象的其它操作
....
[car release];
//Person对象的其它操作
[p driver];
[p release];
}
从上例可看出,Car对象与Person对象都遵守准则一。
问题分析:
从上例可看出,Person对象与Car对象在alloc之后的_referenceCount都等于0,Person对象持有Car对象的引用(执行[p setCar : car]语句之后),但是Person对象与Car对象的_referenceCount都没有发生变化。当执行到[car release]语句时,Car对象的_referenceCount值从1变为0,此时,Car对象被系统回收。接下来执行语句[p driver],在该方法中执行[_car run]语句,发生程序退出。因为此时_car对象所指向的内存空间已被系统回收,在该段内存中的数据为无效数据,故执行语句[_car run]发生run消息不存在。此类错误称为野指针错误。
解决方案:
为了解决类似的野指针问题,在Person对象没有被销毁之前,它引用的Car对象就不应该被销毁。
将Person对象的setCar方法修改为如下:
-(void) setCar : (Car *) car
{
_car = [car retain];
}
上述[car retain]语句的作用是增加变量_referenceCount的值.
在Person销毁时调用Car对象的release方法,代码修改如下:
-(void) dealloc
{
[_car release];
[super dealloc]
NSLog(@"Person对象被销毁了");
}
当Person对象被销毁时,系统调用Person对象的dealloc方法,执行语句[_car release]时,会将Car对象的_referenceCount的值减少1.
现在,再次分析main函数中代码调用流程:
Person对象与Car对象调用alloc方法后,它们的变量_referenceCount的值都变为1。
调用语句[p setCar : car]后,Car对象的_referenceCount的值变为2.
调用[car release]语句时,Car对象的_referenceCount的值变为1.
调用[p driver]语句,接着调用[_car run]语句,在console上输出“Car is running”.
调用[p release]之后,Person对象的_referenceCount值从1变为0,此时系统调用Person对象的dealloc方法销毁Person对象。当调用Person对象的dealloc方法时,先调用的语句是[_car release], 此时Car对象的_referenceCount值从1变为0,系统调用Car对象的dealloc方法销毁Car对象。接着继续执行Person对象dealloc剩余部分代码。
上述提出的解决方案解决了发生野指针问题。是否会发生其它问题呢?接下来进一步分析:
在main函数语句[p setCar : car]之后在次调用语句[p setCar : car],一次alloc两次setCar,Car对象的_referenceCount值从0变为3.
当调用语句[car release]时,变量_referenceCount的值变为2.
当调用语句[p release]时,Person对象的_referenceCount值变为0,系统调用Person对象的dealloc方法,在Person对象的dealloc方法中首先调用语句[_car release],此时Car对象的_referenceCount值从2变为1. Person对象被系统销毁,main函数执行完毕。此时,Car对象发生了内存泄漏,因为Car对象没有被销毁。
该问题的解决方案如下:
修改Person对象的setCar方法如下:
-(void) setCar : (Car *) car
{
if (_car != car)
{
[_car release];
_car = [car retain];
}
}
现在,再次分析main函数代码调用流程:
Person对象与Car对象调用alloc方法后,它们的变量_referenceCount的值都变为1。
调用语句[p setCar : car]后,Car对象的_referenceCount的值变为2.
接着在调用语句[p setCar : car],由于设置的是同一个对象,所以setCar方法什么都没有做。此时Car对象的_referenceCount的值仍然2。
调用[car release]语句时,Car对象的_referenceCount的值变为1.
调用[p driver]语句,接着调用[_car run]语句,在console上输出“Car is running”.
调用[p release]语句时,Person对象与Car对象都可以正确的被销毁。
此次的改进,无论调用多少次setCar方法设置同一个Car对象都不会有问题。
那么,设置一个不同的Car对象是否会有问题呢?
将main函数带吗修改如下:
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
int main(int argc, const char *argv[])
{
Person *p = [ [Person alloc] init];
Car *car = [ [Car alloc] init];
Car *car2 = [ [Car alloc] init];
[p setCar : car];
[p setCar : car2];
//对Person对象的其它操作
......
//对Car对象的其它操作
....
[car2 release];
[car release];
//Person对象的其它操作
[p driver];
[p release];
}
从上述代码中可看出,一个Person对象与两个Car对象都调用了alloc方法,所以它们的_referenceCount的值都为1.
Person对象调用语句[p setCar : car], Car对象car的_referenceCount增加为2。
Person对象调用语句[p setCar: car1], Car对象car1的_referenceCount增加为2,car的_referenceCount值由2变为1.
语句[car2 release]使得car2对象的_referenceCount的值从2变为1.
语句[car release]使得car对象的_referenceCount的值从1变为0,此时car对象被系统回收,释放所占用的内存。
语句[p driver]调用car2对象的run方法,输出“Car is running”。
语句[p release]使得Person对象的_referenceCount由1变为0.系统回收Person对象,调用dealloc函数。在dealloc函数中调用语句[_car release],使得car2对象的_referenceCount值由1变为0.系统回收car2对象所占有的空间。所以该解决方案没有发生内存泄漏也没有发生野指针。
原则3: 当Person对象含有Car对象的引用,Car对象含有Person对象的引用,一端需要修改为assign,一端修改为retain。
Person.h内容入下:
#import <Foundation/Foundation.h>
#interface Person : NSObject
@property (retain) Car *car;
@property int age;
-(void) driver;
#end
Person.c内容如下:
#import "Person.h"
#implementation Person
-(void) dealloc
{
[super dealloc];
[_car release];
NSLog("Person对象被释放了");
}
-(void) driver
{
[_car run];
}
#end
Car.h内容如下:
#interface Car : NSObject
#property (assign) Person *person;
#property int speed;
- (void) run;
#end
Car.m内容如下:
#implementation Car
-(void) dealloc
{
[super dealloc];
NSLog("Car被释放了");
}
- (void) run
{
NSLog("Car is running");
}
#end
例子如下:
#import "Person.h"
#import "Car.h"
int main(int argc, char *argv[])
{
Person *p = [[Person alloc] init];
Car *car = [[Car alloc] init];
[p setCar : car];
[car setPerson : p];
//其它操作
......
[car release];
[p release];
}
上述Person对象与Car对象都调用了alloc函数,所以Person对象与Car对象的_referenceCount都为0.
执行到语句[p setCar : car]时,Car对象的_referenceCount的值从1变为2.
执行到语句[car setPerson : p]时,由于Car对象引用Person对象的方式为assign,所以Person对象的_referenceCount的值仍然为1.
执行到语句[car release]时,Car对象的_referenceCount的值由2变为1.
执行到语句[p release]时,Person对象的_referenceCount的值变为0,系统调用Person的dealloc方法销毁Person对象。在Person对象的dealloc方法中,调用到[_car release]语句时,Car对象的_referenceCount值从1变为0,系统回收Car对象所占用的内存空间。
单个对象或多个对象的手动内存管理就说这么多。
后续会讲IOS内存池管理对象的内存问题。
IOS内存池管理原理:
1. Object-c中内存池使用类NSAutoreleasePool的对象进行表示。
创建NSAutoreleasePool对象主要有两种使用方式:
第一种使用方式如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
…
[pool release];
第二种方式如下:
@autoreleasepool {
…
}
在实际项目开发种,一般会使用第二种方式。该种方式简单,并且可以查看NSAutoreleasePool对象的作用范围。第一种方式不方便查看。但是,两种方式的工作原理类似。
NSAutoreleasePool对象工作原理介绍:在NSAutoreleasePool作用范围内创建的对象,该对象只需要调用autorelease方法加入到NSAutoreleasePool对象池中。当对象池对象被销毁时,对象池对象会对包含在对象池中的所有对象做一次release操作。
使用例子说明如下:
#import <Foundation/Foundation.h>
#import “Person.h”
#import “Car.h”
int main(int argc, const char * argv[])
{
@autoreleasepool{
Person *p = [[[Person alloc] init] autorelease];
}//对象池被销毁
return 0;
}
a. @autoreleasepool执行该语句时,创建了一个对象池对象。对象池的_referenceCount的值变为1.
b. Person *p = [[[Person alloc] init] autorelease]语句执行时,Person对象的_referenceCount的值变为1.并且将该对象加入到对象池。
c. 执行到@autoreleasepool的右花括号时,对象池被销毁,对象池的_referenceCount的值变为0,Person对象的_referenceCount值变为0.所以,Person对象先于对象池被销毁。
ARC机制:
在项目中使用ARC机制,对象的销毁由系统负责。那么,是否使用ARC机制就不会由内存泄露与野指针问题呢?
请看下面的例子:
Person.h内容入下:
#import <Foundation/Foundation.h>
#interface Person : NSObject
@property (nonatomic, strong) Car *car;
@property (nonatomic) int age;
-(void) driver;
#end
Person.c内容如下:
#import "Person.h"
#implementation Person
-(void) dealloc
{
NSLog("Person对象被释放了");
}
-(void) driver
{
[_car run];
}
#end
Car.h内容如下:
#interface Car : NSObject
@property (nonatomic, strong) Person *person;
#property (nonatomic) int speed;
- (void) run;
#end
Car.m内容如下:
#implementation Car
-(void) dealloc
{
NSLog("Car被释放了");
}
- (void) run
{
NSLog("Car is running");
}
#end
例子如下:
#import "Person.h"
#import "Car.h"
int main(int argc, char *argv[])
{
Person *p = [[Person alloc] init];
Car *car = [[Car alloc] init];
[p setCar : car];
[car setPerson : p];
//其它操作
......
return 0;
}
代码分析:
a. Person *p = [[Person alloc] init];执行该语句,Person对象的_referenceCount的值变为1.
b. Car *car = [[Car alloc] init];执行该语句,Car对象的_referenceCount的值变为1.
c. [p setCar : car];执行该语句,Car对象的_referenceCount值为2.因为Car对象被定义为强引用(参考Person.h文件中语句:@property (nonatomic, strong) Car *car;)。
d. [car setPerson : p];执行该语句,Person对象的_referenceCount的值为2.因为Person对象被定义为强引用(参考文件Car.h文件中语句:@property (nonatomic, strong) Person *person;)。
e. main函数执行完退出时,Person对象_referenceCount值变为1,Car对象的值变为1。
从上述分析中可看出,main函数已退出,但是Person对象与Car对象没有被销毁,从而发生了内存泄漏。
测试验证Person对象与Car对象被销毁的方法:
查看console上是否输出如下两行log:
Person对象被释放了
Car被释放了
解决方案:
Car.h头文件中:
将该语句@property (nonatomic, strong) Person *person;
修改为@property (nonatomic, weak) Person *person;
因为weak引用不会导致对象的_referenceCount值发生变化。
代码修改后,分析步骤如下:
a. Person *p = [[Person alloc] init];执行该语句,Person对象的_referenceCount的值变为1.
b. Car *car = [[Car alloc] init];执行该语句,Car对象的_referenceCount的值变为1.
c. [p setCar : car];执行该语句,Car对象的_referenceCount值为2.因为Car对象被定义为强引用(参考Person.h文件中语句:@property (nonatomic, strong) Car *car;)。
d. [car setPerson : p];执行该语句,Person对象的_referenceCount的值仍然1.因为Car对象对Person对象的引用为弱引用,不会导致Person对象的_referenceCount值发生变化。
e. main函数执行完退出时,Person对象_referenceCount值变为0,Person对象被销毁,Car对象的值变为0,Car对象被销毁。
所以,内存泄露问题得到解决。
总结:
1. 手动内存管理分为单对象内存管理与多对象内存管理:
a.单对象管理需要遵守出现一次new/alloc/copy/retain,使用完对象后,必须调用一次release。
b.多对象需要遵守下述几条规则:
-需要遵守a中的规则。
-需要在set对象方法中retain一次设置的对象,在dealloc方法中release一次设置的对象。
-当多次set同一个对象时,在set方法中不需要retain,也不需要release该对象。直接返回设置对象。
-两个对象相互strong引用时,需要将一端设置为strong引用,一端设置为weak引用。
2. 内存池管理:
a. 只要在NSAutoreleasePool对象的作用范围内,创建的对象调用autorelease方法加入对象池。当对象池销毁时,对象池会将对象池中的所有对象调用一次release一次。
b. 内存池管理方式也存在手动内存管理中的对象相互引用导致内存泄露问题。解决方案类似。
3. ARC内存管理:
ARC中也同样存在对象相互引用导致内存泄露问题。