OC内存管理

现在是求职季,OC的面试,肯定少不了内存管理,尽管现在基本都是ARC的项目,应该没有谁还在折腾MRC吧,不过这两天貌似有看到一个开源LeagueofLegends,还真是MRC,那会我看着,我只想说,哥们,你怎么如此蛋疼,没有别的意思,纯属闲扯,好吧,进入今天的话题。

记得从Xcode4.x版本后,具体是不是也不是特别记得了,Xcode新建立的工程,都默认勾选了支持ARC,不过我们可以在Build Settings里面将Objective-C Automatic Reference Counting 设为NO就可以切换成MRC了。

由于移动设备的内存极其有限,再加上,苹果为了让系统流畅,所以每个app所能占用的内存也是有限的,当app占用的内存较多时,就会收到内存警告,以代码的角度来说,程序里面的didReceiveMemoryWarning方法将会被调用,这时,你就需要考虑回收一些不需要使用的内存空间了,比如回收一些不需要的使用对象,清空一些不需要的缓存。如果不处理,当超过一定限度时,系统就会强制杀死你的APP,也就是我们常见的闪退现象。

在OC里面,任何继承了NSObject的对象都需要进行内存管理,因为他们是存放于堆里面的,而对于基础数据类型(int、double、float…)则不在此范围,因为他们是放在栈里面的,栈内存会被系统自动回收,而堆内存则需要由程序员来自己管理,后来苹果为了程序员能把更多精力放在业务上,所以堆出了ARC,但并不表示就不会有内存问题,只是苹果工程师为我们处理了大部分的,如block循环引用啥的,还是要我们自己处理的。

现在举下例子,定义了一个Person类,然后在main中调用:

1
2
3
4
5
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
1
2
3
4
5
6
7
8
9
10
#import "Person.h"
@implementation Person
- (void)dealloc {
NSLog(@"Person delloc");
[super dealloc];
}
@end
1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
int i = 0;
float k = 0.25;
Person *person = [[Person alloc]init];
//[person release];
}
return 0;
}

在内存中,上面的代码如下图所描述的,
堆栈
i和k由于是基本数据类型,所以他们是会放在栈里面的,而person则为指针变量,当执行完[[Person alloc]init]后,会在堆内存里面分配内存空间,如0xff11,而*person里面则指向了0xff11的地址空间,此时,person的引用计数为1,所以屏蔽了[person release]时,运行结果则不会打印Person delloc,原因是当对象的引用计数不为0时,对象不会被释放,自然也就不会调用Personr dealloc()方法了。

引用计数器:OC对象都有自己的引用计数器一般为4个字节的存储空间,它是一个整数,它表示对象自己正在被多少人引用或者使用的计数,当使用alloc,new或者copy创建一个对象时,对象的引用计数为1,每执行一次retain操作,引用计数加1,每执行一次release操作,则引用计数减1,所以不要字面上理解当执行了release后,对象就释放了,它仅仅是让计数器做-1的操作。所以当引用计数器为0时,系统会自动地给对象发送一条dealloc消息,也就是对象的dealloc方法会被调用,此时系统会回收这个对象,反之它将会一直存在,直到整个程序退出。因此我们可以通过重写对象的dealloc方法用来验证对象是否被销毁,但这并不意味着但我们能直接调用dealloc方法。

注意事项:当对象的引用计数为0时,对象将被回收,内存不再可用,如果我们还继续试图访问其内存空间,将会导致程序崩溃,报野指针错误(即我们常见的EXC_BAD_ACCESS)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
int i = 0;
float k = 0.25;
Person *person = [[Person alloc]init];
[person release];
[person release];
[person release];
}
return 0;
}

如上面这段代码,person的引用计数为1,却release3次,当第一次release后,此时,person已经被释放,当再次调用时,就会报出如下错误:

1
malloc: *** error for object 0x100206050: pointer being freed was not allocated

此时person已经是僵尸对象了,我们可以通过勾选如下选项来捕获:
开启僵尸对象选项
再次运行时,你会看到,程序会定位到第二个release的地方,并给出错误原因
开启僵尸对象后的效果

所以为了避免野指针错误的常见方法为:当对象被销毁后,将指向对象的指针变成空指针,即将person = nil;

苹果官方规定的内存管理原则:

  1. 谁创建谁release : 如果你通过alloc、new或[mutable]copy来创建一个对象,那么你必须调用release或autorelease
  2. 谁retain谁release :只要你调用了retain,就必须调用一次release

现在我们将程序改进一下,Person.h

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>
@class Cat;
@interface Person : NSObject
@property (nonatomic, retain) Cat *cat;
@end

Person.m

1
2
3
4
5
6
7
8
9
10
#import "Person.h"
@implementation Person
- (void)dealloc {
NSLog(@"Person delloc");
[super dealloc];
}
@end

Cat.h

1
2
3
4
5
#import <Foundation/Foundation.h>
@interface Cat : NSObject
@end

Cat.m

1
2
3
4
5
6
7
8
9
10
11
#import "Cat.h"
@implementation Cat
- (void)dealloc {
NSLog(@"Cat dealloc");
[super dealloc];
}
@end

在main文件中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Cat.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
Cat *cat = [[Cat alloc]init];
person.cat = cat;
[cat release];
[person release];
}
return 0;
}

结果输出是:

1
2
2016-03-16 19:32:56.766 2.MRC[22806:8357750] Person delloc
Program ended with exit code: 0

为什么cat在这里的引用计数为1,release了,却没有调用cat的dealloc方法,原因是在于retain,因为Person的变量cat的修改关键字为retain,那会他自动生成set方法时,实际上是这样子的,它会比较传入的cat是不是和本身的一致,如果不一致就先执行一次release,再进行retain操作

1
2
3
4
5
6
-(void)setCat:(Cat *)cat {
if (cat != _cat) {
[_cat release];
_cat = [cat retain];
}
}

所以我们需要在Person对cat进行release,如下所示:

1
2
3
4
5
- (void)dealloc {
[_cat release];
NSLog(@"Person delloc");
[super dealloc];
}

再运行程序时,你会看到如下结果:

1
2
3
2016-03-16 19:38:41.038 2.MRC[22821:8377010] Cat dealloc
2016-03-16 19:38:41.039 2.MRC[22821:8377010] Person delloc
Program ended with exit code: 0

可能有人会有疑问,为什么,retain关键字修饰时自动生成的是代码是那样子的,原因在于调用person.cat = cat时,所以我为了防止继续执行 [cat release]后,cat的引用计数为1,release一次后就被释放了,所以我们必须在set方法时,执行一次retain操作,为的是保住这只cat,如果person.cat = cat这句被反复调用时,就会导致cat被retain多次,所以为了保证引用计数的正确性,我们需要对cat进行比较,如果和_cat是相同对象就跳过,反之,则release旧值,retain新值。

另外对于会改变引用计数的另一个是autorelase,是苹果继release后ARC前推的另一个内存管理,当初始化一个对象后,调用autorelease,会返回对象本身,同时会将对象放到一个自动释放池中,但对象的计数器不变,当自动释放池销毁时,会对池子里面的操作都做一次release操作。

autorelease

好处 :关心对象的释放时间,不用关心什么时候调用release

注意 :占用内存较大对象,不要随便使用autorelease,因为可以导致单位时间内,内存无法及时被释放导致内存占用过大而引发系统警告或者崩溃,占用内存较小的对象则没有多少影响

至此,内存管理的相关内容已介绍完毕,仅是个人理解,写作仅是做为自己的笔记,为后续复习之用,如有不对之处,还望指正,谢谢。


注:版权声明:本文为博主原创文章,未经博主允许不得转载。