iOS渐入佳境之内存管理机制(一):MRC

在 iOS 5/ Mac OS X 10.7 开始导入ARC(自动引用计数)机制,利用 Xcode4.2 可以使用,所以我们现在开发时大多都会依赖ARC来管理内存,确实省去了很多手动管理内存的麻烦,重要的是但是!

但是,ObjC中的内存管理机制跟C语言中指针的内容是同样重要的,要开发一个程序并不难,但是优秀的程序则更测重于内存管理,它们往往占用内存更少,运行更加流畅。如果你不了解iOS管理内存的机制,只会用ARC让系统帮你管理内存,可以说你的知识结构是有缺陷的,在解决一些程序中遇到的bug时会浪费掉大量的时间。所以学习MRC(手动管理内存)来深刻理解iOS内存管理机制还是很有必要的。

这次就来谈谈这看似过时的MRC。

看这篇文章时,你可能需要Xcode关闭ARC。

XCode关闭ARC,在Xcode中关闭ARC:项目属性—Build Settings–搜索“garbage”找到Objective-C Automatic Reference Counting设置为No即可。

如果需要对特定文件开启或关闭ARC,可以在工程选项中选择Targets -> Build Phases -> Compile Sources,在里面找到对应文件,添加flag:

打开ARC:-fobjc-arc
关闭ARC:-fno-objc-arc

ObjC中内存是如何管理的?

ObjC中内存的管理是依赖对象引用计数器(在ObjC中每个对象内部都有一个与之对应的整数retainCount)来进行的,当一个对象在创建(通过alloc,new、copy来创建)之后它的引用计数器为1,当调用这个对象的retain方法之后引用计数器自动在原来的基础上加1,当调用这个对象的release方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的dealloc方法来销毁这个对象。

Objective-C的对象生成于堆之上,生成之后,需要一个指针来指向它。(注意基本类型是由系统自己管理的,放在栈上)

alloc:为一个新对象分配内存,并且它的引用计数为1。调用alloc方法,你便有对新对象的所有权

copy:制造一个对象的副本(克隆体),该副本的引用计数为1,调用者具有对副本的所有权

new:[className new]基本等同于[[className alloc] init];

注意:

  1. retainCount为0的时候才会自动调用dealloc方法,所以可以通过dealloc方法来查看是否一个对象已经被回收,如果没有被回收则有可能造成内存泄露。

  2. 如果一个对象被释放之后,那么最后引用它的变量我们手动设置为nil,否则可能造成野指针错误,而且需要注意在ObjC中给空对象发送消息是不会引起错误的。

野指针错误形式在Xcode中通常表现为:

1
Thread 1EXC_BAD_ACCESS(code=EXC_I386_GPFLT)错误。因为你访问了一块已经不属于你的内存。

如何管理,写代码经验?(内存释放的原则)

谁创建alloc,谁释放,即在一个作用域内(两个大括号包起来的范围),如果有创建(alloc,new、copy),最后在右括号之前都必须release。

那什么时候会隐含用到retain,release方法呢??

@property声明了属性a,那用b给a赋值(someObject.a=b)时就需要考虑b和a的retainCount。

影响retainCount的@property参数有assign,retain,copy。默认为assign。

assign

  1. 直接赋值,不影响retainCount
  2. 用于基本数据类型

retain

  1. [a release] [b retain]
  2. 用于NSObject的子类对象。

copy

  1. [a release],但是建立一个索引计数为1的对象,这个对象和b一样,b.retainCount不变
  2. 用于NSstring。

注意,MRC只有属性标识符,没有变量标识符。变量需要retain或者copy其他变量时,直接调用其retain、copy方法。

自动释放池

自动内存释放使用@autoreleasepool关键字声明一个代码块,如果一个对象调用了autorelase方法,那么当代码块执行完之后,在块中调用过autorelease方法的对象都会自动调用一次release方法。这样一来就起到了自动释放的作用,同时对象的销毁过程也得到了延迟(统一调用release方法)。看下面的代码:
这部分代码来自这里

Person.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Person.h

#import <Foundation/Foundation.h>

@interface Person : NSObject

#pragma mark - 属性
#pragma mark 姓名
@property (nonatomic,copy) NSString *name;

#pragma mark - 公共方法
#pragma mark 带参数的构造函数
-(Person *)initWithName:(NSString *)name;
#pragma mark 取得一个对象(静态方法)
+(Person *)personWithName:(NSString *)name;
@end

Person.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//Person.m

#import "Person.h"

@implementation Person

#pragma mark - 公共方法
#pragma mark 带参数的构造函数
-(Person *)initWithName:(NSString *)name{
if(self=[super init]){
self.name=name;
}
return self;
}
#pragma mark 取得一个对象(静态方法)
+(Person *)personWithName:(NSString *)name{
Person *p=[[[Person alloc]initWithName:name] autorelease];//注意这里调用了autorelease
return p;
}

#pragma mark - 覆盖方法
#pragma mark 重写dealloc方法
-(void)dealloc{
NSLog(@"Invoke Person(%@) dealloc method.",self.name);
[super dealloc];
}

@end

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person1=[[Person alloc]init];
[person1 autorelease];//调用了autorelease方法后面就不需要手动调用release方法了
person1.name=@"Kenshin";//由于autorelease是延迟释放,所以这里仍然可以使用person1

Person *person2=[[[Person alloc]initWithName:@"Kaoru"] autorelease];//调用了autorelease方法

Person *person3=[Person personWithName:@"rosa"];//内部已经调用了autorelease,所以不需要手动释放,这也符合内存管理原则,因为这里并没有alloc所以不需要release或者autorelease

Person *person4=[Person personWithName:@"jack"];
[person4 retain];
}
/*结果:
Invoke Person(rosa) dealloc method.
Invoke Person(Kaoru) dealloc method.
Invoke Person(Kenshin) dealloc method.
*/

return 0;
}

当上面@autoreleaespool代码块执行完之后,三个对象都得到了释放,但是person4并没有释放,原因就不细说了。

通过上面的例子主要想说明一个问题(关于类方法创建对象):

在ObjC中通常如果一个静态方法返回一个对象本身的话,在静态方法中我们需要调用autorelease方法,因为按照内存释放原则,在外部使用时不会进行alloc操作也就不需要再调用release或者autorelase,所以这个操作需要放到静态方法内部完成。

对于自动内存释放简单总结一下:

  1. autorelease方法不会改变对象的引用计数器,只是将这个对象放到自动释放池中;
  2. 自动释放池实质是当自动释放池销毁后调用对象的release方法,不一定就能销毁对象(例如如果一个对象的引用计数器>1则此时就无法销毁,例子中的person4);
  3. 由于自动释放池最后统一销毁对象,因此如果一个操作比较占用内存(对象比较多或者对象占用资源比较多),最好不要放到自动释放池或者考虑放到多个自动释放池;
  4. ObjC中类库中的静态方法一般都不需要手动释放,内部已经调用了autorelease方法;所以静态方法命名时都不要以alloc和new开头,容易误解。

后话

下次会讨论ARC。