我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在OC中对象时存储在堆中的,系统并不会自动释放堆中的内存(注意基本类型是由系统自己管理的,放在栈上)。如果一个对象创建并使用后没有得到及时释放那么就会占用大量内存。其他高级语言如C#、Java都是通过垃圾回收来(GC)解决这个问题的,但在OC中并没有类似的垃圾回收机制,因此它的内存管理就需要由开发人员手动维护。今天将着重介绍OC内存管理:
那么OC中是如何管理内存的呢?OC引入了一个机制:引用计数。OC正是通过引用计数来管理内存。
引用计数(Retain Count)
系统如何知道一个对象的创建、使用和销毁的呢?OC在内存管理上采用了引用计数(retain count),在对象内部保存一个数字,用来表示被引用的次数。alloc、retain、new、copy都会让对象的retain count加1。当销毁对象的时候,系统不会直接调用dealloc方法,而是先调用release,让对象的retain count 减1,当retain count等于0的时候,系统才会调用dealloc方法来销毁对象,从而释放内存,到达管理内存的功能。如下:
HXStudent.h
#import <Foundation/Foundation.h>
@interface HXStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
HXStudent.m
#import "HXStudent.h"
@implementation HXStudent
- (void)dealloc {
NSLog(@"销毁student,name = %@", _name);
[super dealloc];
}
@end
在main函数中使用,如下:
void testAlloc() {
HXStudent *student = [[HXStudent alloc] init];// 调用一次alloc,引用计数为1
student.name = @"shxAlloc";
student.age = 28;
NSLog(@"student retain count :%lu", student.retainCount);// 打印:1
[student release];// 调用一次release,引用计数减1,结果为0
// 系统会自动调用HXStudent对象的dealloc方法,销毁HXStudent对象
// 打印:销毁student,name = shx
// HXStudent的对象销毁后,HXStudent的对象所在的内存就不存在,可是student还保留着HXStudent对象的内存地址,所以要将student所指向的地址置nil
// 如果student指向的地址没有置为nil,此时如果再调用对象release会报错,但是如果此时p已经是空指针了,而OC中给空指针发送消息是不会报错的
student = nil;
// 该句不会报错
[student release];
}
void testRetain() {
HXStudent *student = [[HXStudent alloc] init];// 调用一次alloc,引用计数为1
student.name = @"shxRetain";
student.age = 28;
NSLog(@"student retain count :%lu", student.retainCount);// 打印:1
[student retain];// 调用一次retain,引用计数加1,
NSLog(@"student retain count :%lu", student.retainCount);// 打印:2
[student release];// 调用一次release,引用计数减1,结果为1
NSLog(@"student retain count :%lu", student.retainCount);// 打印:1
[student release];// 又调用一次release,引用计数减1,结果为0
// 系统会自动调用HXStudent对象的dealloc方法,销毁HXStudent对象
// 打印:销毁student,name = shx
// HXStudent的对象销毁后,HXStudent的对象所在的内存就不存在,可是student还保留着HXStudent对象的内存地址,所以要将student所指向的地址置nil
// 如果student指向的地址没有置为nil,此时如果再调用对象release会报错,但是如果此时p已经是空指针了,而OC中给空指针发送消息是不会报错的
student = nil;
// 该句不会报错
[student release];
}
int main(int argc, const char * argv[]) {
testRetain();
return 0;
}
从上面的代码中可以看到,当对象调用alloc、retain的时候引用计数会加1,当调用release的时候引用计数就会减1。当对象的引用计数为0的时候,系统会自动调用dealloc方法来销毁对象。如果没有被销毁则有可能造成内存泄露。如果一个对象被释放之后,那么最后引用它的变量我们手动设置为nil,否则可能造成野指针错误,而且需要注意在OC中给空对象发送消息是不会引起错误的。
手动管理内存有时候并不容易,因为对象的引用有时候是错综复杂的,对象之间可能互相交叉引用,此时需要遵循一个法则:谁创建,谁释放。
假设现在有一个人员HXStudent类,每个HXStudent可能会购买一本书HXBook,通常情况下购买书这个活动我们可能会单独抽取到一个方法中,同时买书的过程中我们可能会多看多本来最终确定理想的书,现在我们的代码如下:
HXStudent.h
#import <Foundation/Foundation.h>
#import "HXBook.h"
@interface HXStudent : NSObject {
HXBook *_book;
}
/**
* 人名
*/
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
/**
* book的set方法
*/
- (void)setBook:(HXBook *)book;
/**
* book的get方法
*/
- (HXBook *)book;
@end
HXStudent.m
#import "HXStudent.h"
@implementation HXStudent
- (void)setBook:(HXBook *)book {
if (_book != book) {
[_book release];
_book = [book retain];
}
}
- (HXBook *)book {
return _book;
}
- (void)dealloc {
NSLog(@"销毁student,name = %@", _name);
[self.book release];
[super dealloc];
}
@end
HXBook.h
#import <Foundation/Foundation.h>
@interface HXBook : NSObject
/**
* 书的价格
*/
@property (nonatomic, assign) CGFloat price;
/**
* 读书活动
*/
- (void)read;
@end
HXBook.m
#import "HXBook.h"
@implementation HXBook
- (void)read {
NSLog(@"价格为%f的书", self.price);
}
- (void)dealloc {
NSLog(@"销毁了价格为%f的book", self.price);
[super dealloc];
}
@end
在main方法中,如下:
int main(int argc, const char * argv[]) {
HXStudent *student = [[HXStudent alloc] init];// 调用一次alloc,引用计数为1
student.name = @"shx";
HXBook *book1 = [[HXBook alloc] init];// book1引用计数:1
book1.price = 18.8;
student.book = book1;// book1引用计数:2
NSLog(@"student retain count :%lu", student.retainCount);// 打印:1
HXBook *book2 = [[HXBook alloc] init];// book2引用计数:1
book2.price = 10.0;
[book1 release];// book1引用计数:1
book1 = nil;
[book2 release];// book2引用计数:0
book2 = nil;
// 调用dealloc打印:销毁了价格为10.000000的book
[student.book read];// 此时student的书是book1,因为book1引用计数为1,book1还存在
[student release];// student引用计数:0
student = nil;
// 调用dealloc打印:销毁student,name = shx
// 在dealloc中有[self.book release];,所以此时student拥有的book1的引用计数为0
// 再调用book的dealloc打印:销毁了价格为18.800000的book
return 0;
}
从上面的代码可以看出student、book1、book2三个对象全部都被销毁。体现了谁创建,谁释放的原则。一个人拥有一本书,只要这个人还在,那本书就在。人不在,所拥有的书也就不在了。
尽管遵循了这个原则,但管理起来还是很麻烦。在讲解property声明属性的时候曾经说到,利用property声明属性不仅能自动生成getter和setter方法,还能解决内存管理方面的问题。上面例子中的set方法为什么不写成:- (void)setBook:(HXBook *)book {
_book = book;
}
如果直接赋值的话,就会有问题。当人还没被释放的时候,书已经没有了。这样就不符合我们的要求。只有这样:
- (void)setBook:(HXBook *)book {
if (_book != book) {// 首先判断要赋值的变量和当前成员变量是不是同一个变量
[_book release]; // 释放之前的对象
_book = [book retain]; // 赋值时重新retain
}
}
因为在这个方法中我们通过[bookretain]保证每次属性赋值的时候对象引用计数器+1,这样一来释放book可以保证人的book属性不会被释放。我们在赋新值之前对原有的值进行release操作。最后在HXStudent的dealloc方法中对_book进行一次release操作(因为setBook中做了一次retain操作)保证_book能正常回收。才能做到人在书在,人亡书亡。
@property声明属性的时候在自动生成的set方法中做了什么?其实上面的setBook方法可以使用@property帮我们自动生成,如下:
@property (nonatomic,retain) HXBook *book;
这样系统就能自动帮我们生成上面的代码。@property声明属性是能做到内存管理的。
但这跟@property的属性参数有关,也就是上面的retain关键字。还有哪些属性参数呢,它们的作用是什么?
参数类别 | 属性参数 | 说明 |
原子性 | Atomic(默认) | 对属性加锁,多线程下线程安全,资源消耗大 |
nonatomic | 对属性不加锁,多线程不安全,读\写速度快 | |
读写属性 | readwrite | 生成get、set方法,默认 |
readonly | 只生成get方法 | |
Set方法处理 | assign | 直接赋值,默认 |
retain | 先release旧值,再retian新值 | |
copy | 先release旧值,再copy新值 |
assign,用于基本数据类型
retain,通常用于非字符串对象
copy,通常用于字符串对象、block
下面通过一个例子,结合图来总结一下。
HXStudent.h
#import <Foundation/Foundation.h>
#import "HXBook.h"
@interface HXStudent : NSObject
/**
* 人名
*/
@property (nonatomic, copy) NSString *name;
@<span style="color:#FF0000;">property (nonatomic, retain) HXBook *book;</span>
@end
HXStudent.m
#import "HXStudent.h"
@implementation HXStudent
- (void)dealloc {
NSLog(@"销毁student,name = %@", _name);
[_book release];
_book = nil;
// 上面两句相当于:self.book = nil;
[super dealloc];
}
@end
HXBook.h
#import <Foundation/Foundation.h>
@interface HXBook : NSObject
/**
* 书的价格
*/
@property (nonatomic, assign) CGFloat price;
@end
HXBook.m
#import "HXBook.h"
@implementation HXBook
- (void)dealloc {
NSLog(@"销毁了价格为%f的book", self.price);
[super dealloc];
}
@end
在main.m中使用如下:
int main(int argc, const char * argv[]) {
HXStudent *student = [[HXStudent alloc] init];// 调用一次alloc,引用计数为1
student.name = @"shx";
HXBook *book = [[HXBook alloc] init];// book1引用计数:1
book.price = 18.8;
student.book = book;// book引用计数:2
NSLog(@"book :%lu", book.retainCount);// 打印:2
NSLog(@"student :%lu", student.retainCount);// 打印:1
[book release];// book引用计数:1
[student release];// 引用计数:0
// 调用dealloc方法,里面会将自身的book执行release,所以打印:销毁student,name = shx,之后再打印book的dealloc方法
student = nil;
return 0;
}
main函数是程序的入口,我们就从main函数开始讲解。当程序执行到student.book= book,在内存中对应的图如下:
执行到[book release],上图中,1号线被释放,内存变为如下图:
执行到[student release],2号线被释放,HXStudent对象引用计数为0,被销毁,内存如下:
因为book是student的属性,student被销毁,调用dealloc方法。将book执行release,book的引用计数也为0,内存情况如下:
最后调用book的dealloc方法,销毁book。(上图是本人自己的理解)
以上全部是基于非ARC环境下的,需要手动来调用retain、release等方法来管理引用计数,进而管理内存。