黑马程序员——ObjC基础—内存管理

来源:互联网 发布:淘宝签字收获代表什么 编辑:程序博客网 时间:2024/05/12 09:00
-----------Java培训、Android培训、IOS培训、.Net培训、期待与您交流!------------ 

本节系统的学习一下OC中的内存管理。
在C语言基础中我们已经了解到,在堆区的结构数据或者对象在程序运行过程中是需要我们自己去清理的(也可以交给系统处理,这是后话)。对于普通malloc出来的数据,我们通过free方法来清空,而为了方便,OC中的对象有一套自己的内存回收机制,我们慢慢来了解。

1、malloc数据的回收
手动malloc出来的空间,我们可以直接通过free来进行释放。因为堆区的数据都是通过指针访问的,free后面的参数为一个指针。
#import <Foundation/Foundation.h>typedef struct{    int a;    int b;    int c;} ss;int main(int argc, const char * argv[]){    // 在堆区为结构体开辟空间    ss *p=(ss *)malloc(sizeof(ss));        // 释放堆空间    free(p);    return 0;}

2、OC对象回收的原理
OC对象采用alloc方法创建,实质上和malloc出来的数据一样,存储在堆区域,在使用完成之后也要释放。在OC中,采用引用计数的方式来管理对象的存储,是怎样的运营机制呢?
原来,在每个OC对象内部都有一个retainCount引用计数,用来记录当前有多少个指针指向该对象,当引用计数的值为0的时候,就会自动触发内存清理的代码,回收空间。你或许要问,一个对象怎么能知道一个指针有没有指向他?当然不能,其实是我们在遵循一个规范,每次创建一个指针指向对象,都手动去修改这个引用计数的值。

3、手动计数
我们要保证对象内retainCount引用计数的值与指针数相同,在较早的Xcode】版本中,需要我们自己手动管理,为了方便学习,我们需要将xcode工程项目编译选项里的Automatic Reference Counting参数设置为 NO,只有这样才能手动管理内存。
    [[NSObject alloc] init]; // 匿名对象,在定义的时候引用计数为1,由于无指针定位该对象,后期无法释放        NSObject *p1 = [[NSObject alloc] init]; // 有一个指针指向对象,引用计数为 1        NSObject *p2 = p1; // 新指针指向对象,此时引用计数还是 1        [p1 retain]; // 为了和指针统一,引用计数+1,变为 2        p2 = nil; // 指针释放或者指向别的对象        [p1 release]; // 为了和指针统一,引用计数-1,变为 1        [p1 release]; // 对象不再使用,引用计数继续-1,变为 0 ,对象销毁        p1 = nil; // 清空指针,防止野指针错误
如果一个对象的属性为一个OC对象,我们一般通过在属性的set方法中对引用指针进行操作,请看下面的例子:
#import <Foundation/Foundation.h>// --------------------------------------| BB 类@interface BB : NSObject@end@implementation BB- (void)dealloc{    NSLog(@"%@ dealloc", self);    [super dealloc];}@end// --------------------------------------| AA 类@interface AA : NSObject{    BB *_bb; // 包含BB类实例}- (void)setBb:(BB*)bb;- (BB*)bb;@end@implementation AA- (void)setBb:(BB *)bb{    if (_bb != bb) { // 检测有没有原对象,以及避免重复赋值        [_bb release]; // 当该属性原来指向另一个对象的时候,需要先将该对象计数 -1        _bb = [bb retain]; // 新对象计数 +1    }}- (BB *)bb{    return _bb;}- (void)dealloc{    NSLog(@"%@ dealloc!", self);    [_bb release]; // AA对象销毁的时候,也要取消对别的对象的引用    [super dealloc];}@endint main(int argc, const char * argv[]){    BB *b1=[[BB alloc] init]; // B计数 1        AA *a1=[[AA alloc] init]; // A计数 1        a1.bb = b1; // B计数 2        [b1 release]; // B计数 1    b1 = nil;        [a1 release]; // A计数 0,销毁,B计数 0,销毁        return 0;}
我们执行后会发现,两个对象都能够释放:
2015-02-17 08:11:57.502 Study[6424:303] <AA: 0x10010a020> dealloc!2015-02-17 08:11:57.505 Study[6424:303] <BB: 0x1001069c0> dealloc
比较喜欢钻研的人可能会尝试不创建指针 b1,而是直接创建出新的BB对象赋值给a1,运行会发现BB对象不能够释放,这是因为在创建出对象后引用计数默认都为1,相当于引用计数和实际的指针数不对应了(因为少了b1),可以发现,内存管理要求我们在创建对象的时候,都要有一个非属性的指针和他对应。要是非要不创建这个指针呢?那就得多release一次就可以了,忘记了,后果严重!

4、简化
上面的例子,我们采用了原始的手动set方法,我们不是已经有@property方法自动帮我们写了嘛,难道需要内存管理就得我们手动写吗,当然不是,我们来尝试一下采用@property的方式来写上面的AA类:
// --------------------------------------| AA 类@interface AA : NSObject@property (nonatomic ,retain) BB *bb; // 采用retain关键词,可以由编译器自动帮我们生成计数代码@end@implementation AA- (void)dealloc{    NSLog(@"%@ dealloc!", self);    [_bb release]; // 这句不能省略    [super dealloc];}@end
这样和上面的效果是一样的。
再来了解一下其他的参数:
a、assign
是直接赋值操作,用于一般的参数,例如:
@property (nonatomic, assign) int age;
他表示:
- (void)setAge:(int)age{    _age = age;}
b、copy
一般用于可以拷贝的对象类型,例如:
@property (nonatomic, copy) NSString *aCopy;
他表示:
- (void)setACopy:(NSString *)aCopy{    if (_aCopy != aCopy) {        [_aCopy release];        _aCopy = [aCopy copy];    }}
对于一些可变的对象,如NSMutableString等,他的值是可以变动的,我们把这样的数据付给对象,我们当然不希望外部能够修改这个对象,采用上面的方式来赋值,实际上得到的是NSMutableString的一个NSString类型的拷贝,这个数据只能通过set方法更改。
关于NSString需要引申出以下几点内容:
——采用alloc方式创建
    NSString *str1=[[NSString alloc] initWithFormat:@"xxx"];
这种字符串对象需要我们自己进行内存清理,而下面的:
NSString *str2=@"xxx";
则是系统自己进行清理的字符串,不需要我们去管理。
对于上面的copy方法,如果方法对象是一个可变数据,结果是copy出一个新的对象(计数为1),而如果是一个不可变对象,则不会拷贝出新的对象,只是增加一个引用(计数+1),不违背引用计数法则。
注意:以上的对象都需要在dealloc方法中进行release操作(计数-1操作)。

4、特殊情况(循环引用)
为了描述这种情况,我们举一个小例子:假设有一个girl类和一个boy类,在girl类中有一个boyFriend属性,在boy类中有一个girlFriend属性。我们按照头文件和实现分开的方式来组织代码:
—Boy.h 文件
#import <Foundation/Foundation.h>@class Girl; // 包含Girl@interface Boy : NSObject@property (nonatomic , retain) Girl *girlFriend;@end
—Boy.m 文件
#import "Boy.h"#import "Girl.h" // 包含Girl@implementation Boy// 监听对象销毁- (void)dealloc{    NSLog(@"Boy dealloc!");    [_girlFriend release]; // 释放Girl对象    [super dealloc];}@end

—Girl.h 文件
#import <Foundation/Foundation.h>@class Boy; // 包含Boy@interface Girl : NSObject@property (nonatomic, retain) Boy *boyFriend;@end
—Girl.m 文件
#import "Girl.h"#import "Boy.h" // 包含Boy@implementation Girl// 监听对象销毁- (void)dealloc{    NSLog(@"Girl dealloc!");    [_boyFriend release]; // 释放Boy对象    [super dealloc];}@end
我们可以看出,这两个对象相互包含对方,在头文件中也是相互包含对方的头文件,在这里我们使用了@class来包含对方,是为了防止两个对象循环包含对象导致错误,这是规范的写法。
接下来我们按照正常的使用方法,来看看这两个对象能否顺利销毁:
—main.m 文件
#import <Foundation/Foundation.h>#include "Girl.h"#include "Boy.h"int main(int argc, const char * argv[]){    Girl *girl = [[Girl alloc] init];    Boy *boy = [[Boy alloc] init];        // boy对象和girl对象相互包含    girl.boyFriend = boy;    boy.girlFriend = girl;        [girl release]; // girl对象释放自己,但是此时boy对象还在引用girl对象,无法销毁    [boy release]; // boy对象释放自己,但是此时girl对象还在引用boy对象,无法销毁        return 0;}
这样由于相互引用双方都无法释放的情况称之为循环引用,为了保证对象的正常释放,一般需要互相包含对方的两个对象,他们的@property中的参数,一个用retain,另一个用assign,在使用的时候,retain的对象要先释放。

5、自动释放池
前面的例子中,每创建一个对象,或者每增加一次引用,就需要在适宜的地方有一个release操作与之对应,这样的写法很麻烦,而且容易出错。Xcode引入了自动释放池来帮助我们释放这些对象,自动释放池是一种特殊的对象,在该释放池对象创建的区间内所创建的对象如果调用autorelease方法,就相当于放入了该自动释放池。在该释放池结束的时候,会自动release一次所有放入该释放池内的对象。自动释放池使用格式如下:
IOS5.0之前版本:
 NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];     [对象 autorelease];  [pool release];// IOS中写法 // [pool drain]; // MAC中写法
IOS5.0之后版本:
 @autoreleasepool {    [对象 autorelease]; }
值得注意的是,在上面循环引用的例子里,两个对象谁先release是有要求的,写反了会导致出错,如果我们采用自动释放池尤其需要注意这一点,因为自动释放池是从放入该池的最后一个对象向前依次释放的,这个时候,两个对象创建的顺序需要反过来。
autorelease方法返回的是对象本身,在实际使用中,我们一般采用下面的格式书写:
    Person *p1 = [[[Person alloc] init] autorelease];
我们习惯上是在该对象内部构造一个方法,来简化操作:
+ (id)person{    return [[[self alloc] init] autorelease];}
这样以来,我们就可以简单的通过:
    Person *p2 = [Person person];
来创建对象,注意这里的方法名采用和类名相同的格式,同样我们也可以自定义出带初始化参数的方法,不再赘述。
值得一提的是,在Foundation框架中的对象大都采用类似的方式来封装对象的创建,我们使用该框架提供的对象时,只要不是直接通过alloc创建的对象,都是默认加入自动释放池的。我们自己设计对象也要尽量采用这样的方式封装创建方法。

6、ARC
ARC全程Automatic Reference Counting,自动引用计数,是IOS5之后为我们提供的一个新的内存管理方法,他说白了,就是一个由编译器给我们提供的一个工具,在前面例子中我们所有retain、release、autorelease等操作,都由编译器代为操作,不需要也不允许我们自己去操作,相应的,我们只需要遵循ARC简单的规则,即可不用去理会内存管理的细节。
ARC功能在我们新建工程时,默认是开启的,在前面的例子中为了学习手动管理,我们把这个功能给关闭了,现在可以打开这个功能。
我们看看启用ARC之后,前面循环引用的例子是什么样子:
—Girl.h 文件
#import <Foundation/Foundation.h>@class Boy; // 包含Boy@interface Girl : NSObject@property (nonatomic, weak) Boy *boyFriend; // assign改为weak@end
—Girl.m 文件
#import "Girl.h"#import "Boy.h" // 包含Boy@implementation Girl// 监听对象销毁- (void)dealloc{    NSLog(@"Girl dealloc!");    // 不需要实现对象属性release 和 [super dealloc]}@end
—Boy.m 文件
#import <Foundation/Foundation.h>@class Girl; // 包含Girl@interface Boy : NSObject@property (nonatomic , strong) Girl *girlFriend; // retain改为strong@end
—Boy.m 文件
#import "Boy.h"#import "Girl.h" // 包含Girl@implementation Boy// 监听对象销毁- (void)dealloc{    NSLog(@"Boy dealloc!");    // 不需要实现对象属性release 和 [super dealloc]}@end
—main.m 文件
#import <Foundation/Foundation.h>#include "Girl.h"#include "Boy.h"int main(int argc, const char * argv[]){        Boy *boy = [[Boy alloc] init];        Girl *girl = [[Girl alloc] init];                girl.boyFriend = boy;        boy.girlFriend = girl;        return 0;}
从上面的代码可以看出,采用ARC之后,一句引用计数相关的代码都不用写,甚至自动释放池也不用我们去写(释放池根据实际需要有时是要创建的),运行之后,对象能够正常释放:
2015-02-17 22:08:26.560 Study[11345:303] Boy dealloc!2015-02-17 22:08:26.562 Study[11345:303] Girl dealloc!
我们在上面的代码中采用weak和strong取代retain和release参数,这是因为ARC将对象指针进一步细分为强指针(__strong)和弱指针(__weak)两种类型,弱指针不影响对象的ARC管理,只有强指针影响ARC管理,当指向一个对象的强指针的数量为0的时候,对象会被回收。
强指针:__strong前缀修饰的指针(默认),普通指针
弱指针:__weak前缀修饰的指针,当对象销毁的时候,会自动清空
    [[Person alloc] init]; // 没有指针指向的对象会被直接销毁    __weak Person *p1 = [[Person alloc] init]; // 没有强指针指向的对象会被直接销毁    __strong Person *p2 = [[Person alloc] init]; // 普通对象创建,指针销毁后,对象会被回收


-----------Java培训、Android培训、IOS培训、.Net培训、期待与您交流!------------ 
0 0