内存管理
在iOS中的app,会出现crash(闪退),基本上都是内存出了问题。
出现内存问题的原因,基本上有两种,内存溢出和野指针。
iOS会给每一个app都分配一定的内存。基本上都是固定平分的内存。因此,我们在开发一个app的时候,需要对内存进行管理,否则容易出现内存问题,导致闪退。
内存溢出,在固定的内存里,若是不释放内存,而是往内存里放入大量的数据时,就会导致内存溢出。导致闪退。
野指针:野指针就是,对象所占的内存空间已经被系统回收(指针已经释放),但是指针没有置空,这个指针就会继续指向这个对象数据所在的内存,还能读出里边的数据。此时就更应该注意野指针所带来的危害。
管理范围:任何继承于NSObject的对象,对其他的基本数据类型无效。
内存的管理方式
gc(垃圾回收机制):
只在oc 2.0后才支持,iOS不支持垃圾回收机制
MRC(手动引用计数):
即程序员手动管理内存,也就是开辟内存后和使用内存过后的释放。通过程序员自己写的代码来控制。
ARC(自动引用计数):
ARC不是垃圾回收,而是释放空间。这里的arc是允许程序员开辟空间而不用手动去release自己开辟的空间。arc的本质也是mrc。
引用计数
每个对象都有自己的引用计数器,表示对象所指向的那个实际内存空间一共有几个指针指向它。
alloc
alloc一般用在创建对象,主要是为了分配内存,然后把引用计数+1。
//alloc: //功能1:分配内存 //功能2:将引用计数置为1 Person *p1 = [[Person alloc]init]; NSLog(@"%ld",[p1 retainCount]);2015-04-23 11:10:14.508 OClesson9_内存管理[1029:40725] 1 |
retainCount,用来计算引用计数,但是苹果公司强烈要求不要用retainCount作为编码依据,因为在很多时候,系统也会不知道在什么情况下指向我们的对象或者其他对象。所以这个retainCount得出来的结果不准确。
retain
retain的作用,就是将引用计数 +1。
当引用计数 +1时,表示又友一个指针指向它,如果没有特殊要求,不要单独retain,不然会造成内存泄漏。
//retain //功能:将引用计数+1 //注意:当引用计数 +1 时,表示又有一个指针要指向它。如果没有特殊要求,不要单独retain,不然会内存泄露。 Person *p2 = [p1 retain]; NSLog(@"%ld",[p1 retainCount]);//2015-04-23 11:11:48.156 OClesson9_内存管理[1041:41178] 2 |
copy
标记为copy的实例变量或属性,所在的类必须要遵循NSCopying协议,并实现copyWithZone:方法。
copy标记后,开辟另一块空间,并把原来空间的东西拷贝到新的空间,原来的引用计数不变,新开辟的空间引用计数为1。
copy分为浅拷贝和深拷贝。
浅拷贝:
浅拷贝:又叫拷贝指针。就是把自己的地址再赋给自己。并让引用计数+1。
深拷贝:
深拷贝就是具体的实现copyWithZone:方法时,在里边创建对象,开辟空间,并把内容拷贝到新空间。
一个例子:
Person.h
#import <Foundation/Foundation.h> @interface Person : NSObject<NSCopying> @property (nonatomic,copy)NSString *name; @end |
解释:
1、@property (nonatomic,copy)NSString *name;声明一个name属性,attitude为非原子保护nonatomic和copy的,此时就必须让Person类遵循<NSCopying>协议。
2、Person.m
#import "Person.h" @implementation Person
- (void)dealloc{ [_name release]; [super dealloc]; } //浅拷贝 //又叫拷贝指针 //- (id)copyWithZone:(NSZone *)zone{ // return [self retain]; //}
//深拷贝 - (id)copyWithZone:(NSZone *)zone{ //创建新对象,而且创建新空间 Person *p = [[Person allocWithZone:zone]init]; //给新创建的对象的实例变量赋原来的值 p.name = self.name; return p; } @end |
解释:
1、return [self retain];是浅拷贝的形式,就是让自己指向自己的地址赋给自己,然后retain使引用计数 +1。
2、p.name = self.name;把原来name(name是个指向name所指向那块空间的一个地址)的地址赋给p对象的name。这样,就能让p对象的name指向了原来的地址所指向的内存。
main.m
#import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *p1 = [[Person alloc]init]; p1.name = @"贝爷业";
Person *p2 = [p1 copy];
//地址一样,说明是浅拷贝 NSLog(@"%p",p1);//2015-04-23 17:06:07.893 OCLesson9_NSCopying[2439:129348] 0x100206900 NSLog(@"%p",p2);//2015-04-23 17:06:07.894 OCLesson9_NSCopying[2439:129348] 0x100206900
//地址不一样,说明是深拷贝 NSLog(@"%p",p1);//2015-04-23 17:13:08.601 OCLesson9_NSCopying[2468:131407] 0x100206900 NSLog(@"%p",p2);//2015-04-23 17:13:08.602 OCLesson9_NSCopying[2468:131407] 0x100206b20
NSLog(@"%@",p2.name);//2015-04-23 17:14:32.384 OCLesson9_NSCopying[2481:131971] 贝爷业
[p1 release]; p1 = nil;
} return 0; } |
解释:
1、[p1 release];是将p1释放。
2、p1 = nil;是将p1释放后,把指针置空。若是没有这句,则会使得p1仍然能读取原来的数据,成为野指针。
assign
assign一般用来标记标量(不带 * 号的,或者基本数据类型)和代理delegate。
当然,也可以用assign来标记变量,但是需要重写getter、setter方法,否则就会使用assign自己的getter、setter方法。由于assign自己的getter、setter方法并没有对野指针和内存泄漏进行相应处理,会导致assign标记的变量可能出现野指针或者内存泄漏。
后面会降到具体的实现过程。
release
release是释放一个对象,并使得引用计数 – 1,表示减少一个指针指向这个内存。当引用计数为0 时,系统就会回收这块内存。
注意:我们在mrc下创建对象时,应该在后面写上配套的释放方法,也就是把对象release,并置空。因为在编辑代码过程中,随时会忘记对对象release。release释放对象是按照语句执行的顺序释放的。
autorelease
autorelease,与@autoreleasepool(自动释放池)配套使用。
在创建一个对象后,用autorelease来释放的话,需要注意与release释放的不同。
@autoreleasepool实际上是一个栈,在pool内,每遇到一个对象用autorelease释放,就会将这个对象压入@autoreleasepool的栈中。一直到语句执行到@autoreleasepool的作用域的花括号时,就会将这个对象出栈,一个一个的匹配地址,然后释放掉。所以就会出现先autorelease的对象,最后才被释放掉。
由于@autoreleasepool是一个栈结构,因此自己也有一定的容量,当创建的对象过多时,如果不及时释放的话,就会导致栈溢出。出现程序崩掉的情况。所以,有时候,考虑好数据量后,再考虑把@autoreleasepool写在哪个地方(防止溢出)。
一个例子:
Student.h
#import <Foundation/Foundation.h>
@interface Student : NSObject
@property(nonatomic , copy)NSString *name; @property(nonatomic , assign)NSInteger age;
@end |
Student.m
#import "Student.h"
@implementation Student - (void)dealloc{ NSLog(@"开始销毁实例变量%@...",_name); [_name release]; //标量不需要release。 NSLog(@"实例变量%@已销毁...",_name); [super dealloc]; } @end |
main.m
#import <Foundation/Foundation.h> #import "Person.h" #import "Student.h"
int main(int argc, const char * argv[]) { //自动释放池 @autoreleasepool { //练习 Student *s1 = [[Student alloc]init]; s1.name = @"黄飞鸿"; s1.age = 19; [s1 release]; s1 = nil; //retainCount,不能作为编程依据,因为不知道系统会调用多少次。 Student *s3 = [[Student alloc]init]; s3.name = @"s3"; //release一执行,引用计数-1。 // [s3 release];
//autorelease,除了自动释放池(@autoreleasepool),引用计数-1。 [s3 autorelease];
Student *s4 = [[Student alloc]init]; s4.name = @"s4"; //release是按顺序释放 // [s4 release]; //autorelease //autoreleasepool是一个栈结构,先进后出。也就是s5先释放,s4后释放 [s4 autorelease];// Student *s5 = [[Student alloc]init]; s5.name = @"s5"; // [s5 release]; [s5 autorelease]; } return 0; } |
autoreleasepool的几个例子:
Student.h
#import <Foundation/Foundation.h> @interface Student : NSObject @property(nonatomic , copy)NSString *name; @property(nonatomic , assign)NSInteger age; @end |
Student.m
#import "Student.h" @implementation Student - (void)dealloc{ NSLog(@"开始销毁实例变量%@...",_name); [_name release]; //标量不需要release。 NSLog(@"实例变量%@已销毁...",_name); [super dealloc]; } @end |
main.m
#import <Foundation/Foundation.h> #import "Student.h" //安全释放的宏定义(装逼小技巧) //所有的宏定义都应该加do{}while(0) #define SAFERELEASE(pointer)do{[pointer release];pointer = nil;}while(0) //do{}while()就是为了限制里边变量的作用域 #define TEST do{int a = 10; int b = 20;}while(0)
int main(int argc, const char * argv[]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init]; Student *s1 = [[Student alloc]init]; s1.name = @"aaaa"; [s1 autorelease];
[pool release]; NSLog(@"%@",s1.name);//除了释放池,s1没有被限定,就变成了——野指针
// @autoreleasepool { // for (int i = 0; i < 10000000; i ++) { // //autoreleasepool创建位置要合适 // @autoreleasepool { // Student *s = [[Student alloc]init]; // //使用 // [s autorelease];//入栈,此时应该把池放到for里 // } // } @autoreleasepool { Student *s1 = [[Student alloc]init]; Student *s2 = [s1 retain]; //谁污染谁治理(谁开辟的空间,谁就负责释放) [s1 release]; [s2 release]; }
return 0; } |
@autoreleasepool写出来的时候,应该加上作用域,也就是加上花括号,因为若是不加上作用域,会导致后面释放的指针,在释放后还可以继续使用,一直到程序结束为止。成为野指针。
dealloc
dealloc方法是从父类NSObject继承过来的。
它的作用就是检测某个对象的引用计数是否为0 ,如果为0,则系统会自动调用dealloc方法,销毁对象自己的实例变量。
dealloc方法一般写在.m实现文件中,并且重写dealloc方法应该注意:
1、销毁自己的意思,就是销毁引用计数为0的那个对象的实例变量。
2、在写dealloc方法时,最先把[ super dealloc ]写在dealloc方法里的最后一行。然后在[ super dealloc ]前面几行再编辑其他代码。
3、在一个类中,声明一个变量(属性)的时候,应该在下面写上对应的dealloc。
Person.m
#import "Person.h"
@implementation Person
//当引用计数为0 时,自动调用这个方法。
- (void)dealloc{ //当发现引用计数为0的时候,直接销毁对象
//销毁自己 //就是销毁自己的实例变量。 NSLog(@"我完蛋了..."); //全部实力变量在dealloc调用前,要全部release。 //除了标量外。 [_name release]; //必须写在最后面。 [super dealloc]; } @end |