OC内存管理总结

来源:互联网 发布:精益数据分析 微盘 编辑:程序博客网 时间:2024/05/16 07:28

1.为什么要管理内存

首先看这样一段代码:

[objc] view plaincopy
  1. int main(int argc, const charchar * argv[])  
  2. {  
  3.     int a = 10;  
  4.     BOOL b = YES;  
  5.     char c = 'w';  
  6.     return 0;  
  7. }  

像a、b、c这样的基本数据类型的局部变量存放在栈上,函数执行结束时这些存储单元自动被释放。

在看看这样的代码:

[objc] view plaincopy
  1. int main(int argc, const charchar * argv[])  
  2. {  
  3.     Person *p = [[Person alloc] init];  
  4.     [p setAge:10];  
  5.     NSLog(@"%d", [p age]);  
  6.     return 0;  
  7. }  
像Person这样的对象类型,分配在堆上,在上面的代码中,[[Person alloc] init]在堆上创建了一个Person类型的对象 Person类型的指针p指向了这个对象,而指针p类型保存在栈上,当程序结束时p所占用的内存被清空,而存储在堆上得对象没有被释放,依然放在内存中。OC项目中的代码中处处都是这样的代码,如果不进行内存管理,程序运行过程会不断地加大内存开销,最终可能导致程序崩溃。

2.引用计数器

Ø   每个OC对象都有自己的引用计数器,是一个整数,表示“对象被引用的次数”,即有多少人正在使用这个OC对象

Ø   每个OC对象内部专门有4个字节的存储空间来存储引用计数器

Ø   当使用allocnew或者copy创建一个新对象时,新对象的引用计数器默认就是1

Ø   当一个对象的引用计数器值为0时,对象占用的内存就会被系统回收。换句话说,如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收,除非整个程序已经退出


我们可以使用引用计数器来进行内存管理,比如:

当程序结束时,p不存在了,而p指向的对象的对象的引用计数器由1变成了0,意味着没有引用它的指针了,这时候可以将堆中对象占用的内存回收,进行合理的内存管理。现在的问题就是如何进行管理对象的引用计数器

OC语言中定义了下面几种对引用计数器的操作:

Ø   给对象发送一条retain消息,可以使引用计数器值+1(retain方法返回对象本身)

Ø   给对象发送一条release消息,可以使引用计数器值-1

Ø   可以给对象发送retainCount消息获得当前的引用计数器值

由此可以得到启发,每当创建对象或者复制对象时可以使用retain将其"引用计数器"的值+1,而不再使用引用它的指针时,将对象的"引用计数器"的值-1,问题就来了:如何在对象的"引用计数器"的值为0的时候将对象销毁呢:

对象的销毁

Ø   当一个对象的引用计数器值为0时,那么它将被销毁,其占用的内存被系统回收

Ø   当一个对象被销毁时,系统会自动向对象发送一条dealloc消息

Ø   一般会重写dealloc方法,在这里释放相关资源,dealloc就像对象的遗言

Ø   一旦重写了dealloc方法,就必须调用[superdealloc],并且放在最后面调用

Ø   不要直接调用dealloc方法

Ø   一旦对象被回收了,它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)

下面对Person类的deallloc方法进行重写:

[objc] view plaincopy
  1. @implementation Person  
  2. // 当一个Person对象被回收的时候,就会自动调用这个方法  
  3. - (void)dealloc  
  4. {  
  5.     NSLog(@"Person对象被回收");  
  6.     // super的dealloc一定要调用,而且放在最后面  
  7.     [super dealloc];  
  8. }  

对retain、release、retainCount做一些简单的测试:

[objc] view plaincopy
  1. #import <Foundation/Foundation.h>  
  2. #import "Person.h"  
  3. int main(int argc, const charchar * argv[]) {  
  4.       
  5.     Person *p = [[Person alloc] init]; // 执行后p指向的对象retainCount为1  
  6.       
  7.     NSUInteger c = [p retainCount]; // 获取p指向的对象的retainCount  
  8.       
  9.     NSLog(@"计数器:%ld", c);  
  10.       
  11.       
  12.     [p retain];  // 执行完这句之后retainCount变为2<span style="font-family: 宋体;"> </span>  
  13.     //   
  14.     [p release];  // retainCount为1  
  15.     [p release];   // retainCount为0 系统自动调用Person的dealloc方法   
  16.     return 0;  
  17. }  
程序运行后输出的结果是:

[objc] view plaincopy
  1. 2014-12-24 15:40:35.594 01-引用计数器的基本操作[5044:303] 计数器:1  
  2. 2014-12-24 15:40:35.602 01-引用计数器的基本操作[5044:303] Person对象被回收  
使用引用计数器要特别注意:

√注意点一:给已经释放的对象发送了一条setAge消息-------->闪退

√注意点二:给空指针发送消息不会报错 OC中不存在空指针错误

[objc] view plaincopy
  1. #import <Foundation/Foundation.h>  
  2. #import "Person.h"  
  3. int main(int argc, const charchar * argv[]) {  
  4.     //  
  5.     Person *p = [[Person alloc] init];  
  6.     [p retain];  
  7.     //  
  8.     [p release];  
  9.     [p release];  
  10.     p.age = 10// 给已经释放的对象发送了一条setAge消息-------->闪退  
  11.     return 0;  
  12. }  
这时候说指针p指向了僵尸对象(对象的内存已经被释放了),此时p被成为野指针

这时候有这样一种解决方案将p赋值为ni,因为对空指针(值为nil的指针,基本数据类型而言值为0)调用方法时,程序什么也不做。代码如下:

[objc] view plaincopy
  1. #import <Foundation/Foundation.h>  
  2. #import "Person.h"  
  3. int main(int argc, const charchar * argv[]) {  
  4.     //  
  5.     Person *p = [[Person alloc] init];  
  6.     [p retain];  
  7.     //  
  8.     [p release];  
  9.     [p release];  
  10.       
  11.     p = nil;// 对象计数器rc减为0时调用 以此来消除野指针  
  12.   
  13.     p.age = 10;// 给空指针发送消息不会报错 OC中不错在空指针错误  
  14.     [p release];// 给空指针发送消息不会报错 OC中不错在空指针错误  
  15.     return 0;  
  16. }  

3.多对象内存管理

在实际开发中,可能遇到这样的情况,一个类A的成员变量类型也是一个类B,对成员变量B类型对象的引用就不只是B类型,可能还有A类型。这样容易引发这样两种问题,①B类型的对象被销毁了A成员仍然在引用它,②A类型的对象销毁顺带着将B类型的成员变量销毁,但仍有B类型的指针指向该对象。只要按照以下原则使用引用计数,就可以对内存进行合理的管理

原则1.       谁创建,谁release

Ø   如果你通过alloc、new或[mutable]copy来创建一个对象,那么你必须调用release或autorelease

Ø   换句话说,不是你创建的,就不用你去[auto]release 

原则2.       谁retain,谁release

Ø   只要你调用了retain,无论这个对象是如何生成的,你都要调用release 

例如Person类中包含了一个Book类,规范的设计代码应该是这样:

[objc] view plaincopy
  1. #import "Person.h"  
  2.   
  3. @implementation Person  
  4. - (void)setBook:(Book *)book  
  5. {  
  6.     _book = [book retain];  
  7. }  
  8. - (Book *)book  
  9. {  
  10.     return _book;  
  11. }  
  12.   
  13. - (void)dealloc  
  14. {  
  15.     [_book release];  
  16.     NSLog(@"Person对象被回收");  
  17.     [super dealloc];  
  18. }  
  19. @end  
[objc] view plaincopy
  1. </pre><pre name="code" class="objc">#import <Foundation/Foundation.h>  
  2. #import "Book.h"  
  3. #import "Person.h"  
  4. int main(int argc, const charchar * argv[]) {  
  5.       
  6.     Book *b = [[Book alloc] init];  
  7.       
  8.     Person *p1 = [[Person alloc] init];  
  9.       
  10.     [p1 setBook:b];  
  11.       
  12.       
  13.     [p1 release];  
  14.     p1 = nil;  
  15.       
  16.     [b release];  
  17.     b = nil;  
  18.     return 0;  
  19. }  

上面的程序对多个对象内存管理很好地进行了总结:

 1.你想使用(占用)某个对象,就应该让对象的计数器+1(让对象做一次retain操作)// 如 Person 要使用Book
 2.你不想再使用(占用)某个对象,就应该让对象的计数器-1(让对象做一次release) 
 3.谁retain,谁release // Person retain Book ,Person就要release Book
 4.谁alloc,谁release // Book alloc Book对象,Book就要release Book对

4.set方法完善---针对内存管理

在刚刚的多对象内存管理中,仍让存在一些不合理的地方:

假设有这样的场景,人拥有一辆车,按照刚才的方法先创建一辆车carA,设置人的车为carA,但是当人要换车时人并没有对carA进行release(因为人 没有调用dealloc方法,但实际上人已经不再引用carA了,应当将carA的rc减一)。解决的方案是在Person的set方法中将旧的车release一次:

[objc] view plaincopy
  1. - (void)setCar:(Car *)car  
  2. {     
  3.     // 对当前正在使用的车(旧车)做一次release  
  4.     [_car release];  
  5.     // 对新车做一次retain操作  
  6.     _car = [car retain];      
  7. }  

这样似乎能解决问题,但是如果新传入的汽车仍然是carA(现在的车),且在执行[p setCar:carA];之前carA被release了一次(也就是说carA现在的rc是1),此时执行setCar方法,就会引起野指针问题。因此再对set方法进行完善:

[objc] view plaincopy
  1. - (void)setCar:(Car *)car  
  2. {  
  3.     if (car != _car)  
  4.     {  
  5.         // 对当前正在使用的车(旧车)做一次release  
  6.         [_car release];  
  7.         // 对新车做一次retain操作  
  8.         _car = [car retain];  
  9.     }  
  10. }  
总结

内存管理代码规范:

1.只要调用了alloc,必须有release(autorelease)

2.set方法的代码规范

  1>.基本数据类型:直接赋值

[objc] view plaincopy
  1. - (void)setAge:(int)age  
  2. {// 基本数据类型 不需要管理内存  
  3. _age = age;  
  4. }  
  2>.OC对象类型

[objc] view plaincopy
  1. - (void)setCar:(Car *)car  
  2. {  
  3.     // 1.先判断是不是新传进来的对象  
  4.     if(car != _car)  
  5.     {  
  6.         // 2.对旧的对象做一次release  
  7.         [_car release];  
  8.   
  9.         // 3.对新传进来的对象做一次retain  
  10.         _car = [car retain];  
  11.     }  
  12. }  
   3.dealloc方法的代码规范

1>一定要调用[super dealloc]且放在最后

2>.对当前对象所拥有的其他对象做一次release

[objc] view plaincopy
  1. - (void)dealloc  
  2. {  
  3.     [_car release];  
  4.     [super dealloc];  
  5. }  

利用@property参数对内存进行管理

默认情况下@property生成set方法只是简单的赋值操作:

在Person类的声明中添加这行代码

[objc] view plaincopy
  1. @property Book *book;  
意味着编译器在实现中帮助生成的set方法是这样的

[objc] view plaincopy
  1. - (void)setBook:(Book *)book  
  2. {  
  3.     _book = book;  
  4. }  
这样显然是不合理的(刚刚讨论的),若要为@property添加retain关键字:

[objc] view plaincopy
  1. @property (retainBook *book;  
编译器便会添加这样的set方法:

[objc] view plaincopy
  1. - (void)setBook:(Book *)book  
  2. {  
  3.     if (book != _book)  
  4.     {  
  5.         [_book release];  
  6.         _book = [book retain];  
  7.     }  
  8. }  

可见retain参数的作用: 生成的set方法里面,release旧值,release新值。
多么完美的编译器特性。
因此开发中对于成员变量是类类型时@property要添加retain参数,需要注意的是使用NSString作为成员变量类型时也要添加,因为NSString也是了类类型。

5.property参数详解

√1.内存管理相关的参数(三择一)

retain: release旧值、retain新值(适用于OC对象类型)

assign:直接赋值(默认,适用于非OC对象类型)

copy:release旧值, copy新值


√2.是否要生成set方法

readwrite:同时生成settergetter的声明和实现(默认)

readonly:只会生成getter的声明和实现


√3.多线程管理

nonatomic:性能高(一般就用这个)

atomic:性能低(默认)


√4.settergetter方法的名称

setter:决定了set方法的名称,一定要由个冒号:

getter:决定了get方法的名称(一般用在BOOL类型的属性值)

注意:

当成员变量类型为BOOL类型时,get方法方法名一般以is开头

[objc] view plaincopy
  1. @property (getter=isRich) BOOL rich;  

6.类的循环引用

两个类循环引用时(A引用B,B也引用A)

在两个类中在.h分别用@class声明另一个类 m文件中使用#import

// @class仅仅是告诉编译器这是一个类

@class#import的区别

 #import方式会包含被引用类的所有信息,包括被引用类的变量和方法;

@class方式只是告诉编译器在A.h文件中 B *b只是类的声明,具体这个类里有什么信息,这里不需要知道,等实现文件中真正要用到时,才会真正去查看B类中信息

 如果有上百个头文件都#import了同一个文件,或者这些文件依次被#improt,那么一旦最开始的头文件稍有改动,

后面引用到这个文件的所有类都需要重新编译一遍,这样的效率也是可想而知的,而相对来 讲,使用@class方式就不会出现这种问题了

 .m实现文件中,如果需要引用到被引用类的实体变量或者方法时,还需要使用#import方式引入被引用类


MJ总结

 1.@class的作用:仅仅告诉编译器,某个名称是一个类

 

 2.开发中引用一个类的规范

 1>..h文件中用@class声明类

 2>.m文件中用#import来包含类的所有东西

 

 3.两端循环引用解决方案

 1>.一端用retain

 2>.一端用assign

例如Person拥有Card类型的属性,Card类型拥有Person类型的属性:

在Person.h中

[objc] view plaincopy
  1. #import <Foundation/Foundation.h>  
  2. // @class仅仅是告诉编译器这是一个类  
  3. @class Card;  
  4. @interface Person : NSObject  
  5. @property (nonatomicretainCard *card;  
  6. @end  
在Person.m中
[objc] view plaincopy
  1. #import "Person.h"  
  2. #import "Card.h"  
  3.   
  4. @implementation Person  
  5. - (void)dealloc  
  6. {  
  7.       
  8.     NSLog(@"Person被销毁");  
  9.     [_card release]; // 使用retain参数的对象 需要这一句  
  10.     [super dealloc];  
  11. }  
  12. @end  
在Card.h中

[objc] view plaincopy
  1. #import <Foundation/Foundation.h>  
  2. @class Person;  
  3. @interface Card : NSObject  
  4. @property (nonatomic, assign) Person *person;  
  5. @end  
在Card.m中

[objc] view plaincopy
  1. #import "Card.h"  
  2. #import "Person.h"  
  3.   
  4. @implementation Card  
  5. - (void)dealloc  
  6. {  
  7.     NSLog(@"Car被销毁");  
  8. //    [_person release]; // 因为使用的时assign参数 所以不用这一句      
  9.     [super dealloc];  
  10. }  
  11. @end  

测试:
[objc] view plaincopy
  1. #import <Foundation/Foundation.h>  
  2. #import "Person.h"  
  3. #import "Card.h"  
  4. int main(int argc, const charchar * argv[])  
  5. {  
  6.     Person *p = [[Person alloc] init];  
  7.       
  8.     Card *c = [[Card alloc] init];  
  9.     p.card = c;  
  10.     c.person = p;  
  11.       
  12.     [c release];  
  13.     [p release];  
  14.       
  15.     return 0;  
  16. }  
执行的结果为:

[objc] view plaincopy
  1. 2014-12-24 17:44:33.911 07-循环引用[6961:303] Person被销毁  
  2. 2014-12-24 17:44:33.913 07-循环引用[6961:303] Car被销毁  

若是将

[objc] view plaincopy
  1. [c release];  
  2. [p release];  
改为

[objc] view plaincopy
  1. [p release];  
  2. [c release];  
执行的结果为:

[objc] view plaincopy
  1. 2014-12-24 17:48:29.285 07-循环引用[7046:303] Person被销毁  
  2. 2014-12-24 17:48:29.287 07-循环引用[7046:303] Car被销毁  

7.autorelease的使用

√1.autorelease的基本用法

 1>.会将对象放到自动释放池

 2>.当自动释放池销毁时,会对池子里面的所有对象做一次release操作

 3>.方法会返回对象本身

 4>.调用完autorelease方法后,对象的计数器不变

 

 2.autorelease的好处

 1>.不用担心对象释放的时间

 2>.不用关心什么时候调用release

 

 √3.autorelease的使用注意

 1>.占用内存较大的对象不要随便使用autorelease

 2>.占用内存较小的对象使用autorelease,没有太大影响

 

 4.错误写法

 1>.alloc之后调用了autorelease,又调用release 

[objc] view plaincopy
  1. @autoreleasepool  
  2.  {  
  3.     Person *p = [[[Person alloc] init] autorelease];  
  4.     [p release];  
  5.  }  

  2>.连续调用多次autorelease

[objc] view plaincopy
  1. @autoreleasepool  
  2. {  
  3.     Person *p = [[[[Person alloc] init] autorelease] autorelease];  
  4. }  


 5.自动释放池

 1>.IOS程序运行过程中,会创建无数个池子,这些池子都是以栈结构存在(先进后出)

 2>.当一个对象调用autorelease方法时,会将这个对象放到栈顶的释放池

 

 √6.自动释放池的创建方式

 1>.IOS5.0之前 

[objc] view plaincopy
  1. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  
  2.  Person *p = [[[Perosn alloc] init] autorelease];  
  3.  [pool release];// [pool drain];  

 

 2>.IOS5.0开始

 

[objc] view plaincopy
  1. @autoreleasepool  
  2.  {  
  3.     Person *p = [[[Person alloc] init] autorelease];  
  4.  }  

autorelease应用

1.系统自带的方法里面没有包含allocnewcopy说明返回的对象都是autorelease

  [NSString stringWithFormat:...];


2.开发中经常会提供一些类方法,快速创建一个已经autorelease过的对象

1>.创建对象时候不要直接用类名,一般用self

[objc] view plaincopy
  1. + (instancetype)person  
  2. {  
  3.     return [[[self alloc] init] autorelease];  
  4. }  

2>.添加带参数的方法时,先使用1>中创建的方法初始化再赋值

[objc] view plaincopy
  1. + (instancetype)personWithAge:(int)age  
  2. {  
  3.     Person *p = [self person];  
  4.     p.age = age;  
  5.     return p;  
  6. }  

8.ARC机制

arc是一种编译器特性:编译时编译器帮助完成代码 ,不能和垃圾回收混为一谈

而JAVA垃圾回收是运行时特特性。

 强弱指针:指针分2

 1>强指针默认情况下,所有的指针都是强指针 __strong

 2>弱指针__weak


ARC所做的工作:

使用arc机制,每当使用alloc时,编译器都会添加release;

每一个类的dealloc方法中都会增加对类成员变量的release;

ARC的判断准则:只要没有强指针指向对象,就会释放对象

[objc] view plaincopy
  1. // 错误写法(没有意义的写法)  
  2. __weak Person *p = [[Person alloc] init]; // 创建 随即就被销毁了 相当于把空值nil赋给了p  

 

ARC特点(使用时需要注意的地方)

 1>.不允许调用releaseretainretainCount

 2>.允许重写dealloc,但是不允许调用[super dealloc];

 3>.@property的参数

 strong:成员变量是强指针 (适用于OC对象类型)

 weak:成员变量是弱指针 (使用于OC对象类型)

 assign:适用于非OC对象类型

 4>.以前的retain改为用strong,其他不变


设置单个文件是否使用arc (对于导入其他第三方开发框架的情况)

Build Phases->Compile Sources->选择文件->修改文件编译参数Compile Flags

-fno-objc-arc//不需要arc

-f-objc-arc//需要arc

将整个非arc的工程转为arc工程  Edit->Refactor->Convert to Objective-C ARC


使用ARC时如何处理循环引用:

 当两端循环引用的时候,解决方案:

 1.使用ARC

 修改@property参数:一端用strong,一端用weak

 2.ARC

修改@property参数: 一端用retain另一端用assign

0 0
原创粉丝点击