0%

iOS教程(一)iOS内存管理

最近一个月学习了 iOS 开发相关的知识。Objective-C 的内存管理是最基础也是最重要的。以此篇博客作一个记录,并为后来学习 iOS 开发的同学提供一个参考。

OC 内存管理简介

OC 的内存管理不同于 C++ 的内存管理方式。C++ 的内存管理方式相对来说比较简单粗暴,new 出来的内存,不用的时候 delete 掉就行。
OC 的内存管理采用了引用计数的方法。简单的说,就是对每一个对象都保存一个引用计数值,当程序中增加一个对该对象的引用时,引用计数值增加 1;反之,程序中减少一个对该对象的引用时,则引用计数值减少 1。当引用计数值为0是,系统释放对象。

OC内存管理的原则:谁拥有,谁释放

retain & release

首先介绍这两个最基本的方法。对象的 retain 方法将对象的引用计数加 1。对象的 release 方法将对象的引用计数减 1,同时判断对象的引用计数是否为 0,如果为 0,释放对象。

1
2
3
4
5
6
NSString *str = [[NSString alloc] init];    // 引用计数为1
[str retain]; // 引用计数为2
[str release]; // 引用计数为1
[str release]; // 对象释放,str沦为野指针

// str释放后,调用retainCount函数得到的引用计数可能仍旧为1。

autorelease

但是,在某些情况下,只有 retain 和 release 并不能很好的解决问题。例如:

1
2
3
4
5
6
7
- (NSString*)getStr
{
NSString *str = [[NSString alloc] init];
// do some jobs

return str; // str并没有release
}

在上述的例子中,由于调用者需要用到函数返回的 str,因此,不能在 getStr 中 release 掉 str。导致的结果是,函数调用者在获得函数的返回值 str,使用完后调用 release 释放 str。这就违反了在 OC 的谁拥有谁释放的内存管理原则。

按照「谁拥有谁释放」的原则,getStr 创建了 str,所以需要释放掉 str。调用者获取到 str 后,需要拥有这个 str,就需要 retain,并在某个时机点释放掉。

这个时候就需要 autorelease 了!

在这里,要明确一个概念。对象调用 autorelease 函数后,并不是说这个对象就完全由系统管理了,调用 autorelease 函数只代表系统会在将来的某个时候,调用一次该对象的 release 方法。
调用 autorelease 函数后,对象会被添加到自动释放池中。在下一个 runloop 或者其他未来的时刻,自动释放池中的对象都会被 release 一次。自动释放池就是 NSAutoReleasePool 对象啦。

那么,刚刚的 getStr 的函数可以改写成:

1
2
3
4
5
6
7
8
9
- (NSString*)getStr
{
NSString *str = [[NSString alloc] init];
// do some jobs

// str 在函数内创建,并调用 autorelease。由系统 release 一次。
// 满足 OC 的谁拥有谁释放的内存管理原则。
return [str autorelease];
}

那么,调用 getStr 函数的操作也有些许变化。

1
2
3
4
5
NSString *str = [[obj getStr] retain];
// retain and do some jobs

// 用完后 release
[str release];

属性 property

相比于 C++ 的类,OC 的类多了属性这一块东西。大部分情况下,定义一个属性,同时定义了一个成员变量,以及该成员变量的 getter 和 setter。

1
2
3
4
5
@interface MyObject: NSObject

@property (retain, nonatomic) NSMutableArray *array;

@end

对于 MyObject 的对象,可以像如下的方式访问和修改 array 属性:

1
2
3
MyObject *obj = [[[MyObject alloc] init] autorelease];
NSObject *c = [obj.array objectAtIndex:0];
obj.array = [[[NSMutableArray alloc] init] autorelease];

本质上,能这样使用是因为 MyObject 内定义了 _array 成员变量,以及 getArray 和 setArray 方法。

那么,在定义属性时,retain 和 nonatomic 是什么意思呢?在这里只讲内存管理相关的 retain,copy,assign。其他的会在我的另一篇博客中讲解。

  • retain
    当属性定义为retain时,通过obj.array = newArray赋值时,会 retain 新对象,并 release 掉旧对象,像下面这样。
1
2
3
4
5
6
- (void)setArray:(NSArray*)newArray
{
[newArray retain];
[_array release];
_array = newArray;
}

在上述代码中,先 release 旧对象,再 retain 新对象会不会有问题咧?当然有问题!如果 newArray 和 array 是同一个对象,先release 旧对象,那么可能 release 的时候对象就已经释放了,接下来再 retain 就没有什么意义了。

  • copy
    当属性定义为 copy 时,属性赋值会拷贝新对象。
1
2
3
4
- (void)setArray:(NSArray*)newArray
{
_array = copy of newArray;
}
  • assign
    assign 是最简单的一个,一般用于基本类型。属性赋值时不会做额外的操作,直接赋值。
1
2
3
4
- (void)setInteger:(NSInteger)integer
{
_integer = integer;
}
  • 总结
    一般情况,NSString / NSArray / NSDictionary 类型属性定义为 copy,其他 NSObject子类定义为 retain,基础属性定义为 assign。
    对于 retain 和 copy 的属性,需要在 dealloc 中 release。
1
2
3
4
5
6
7
- (void)dealloc
{
[_array release];
_array = nil;

[super dealloc];
}

当为 retain 属性赋值时,需要特别注意。如果一个变量是通过带有alloc / new / copy / create 单词函数新建对象后赋值给 retain 属性,则新建变量需要 autorelease。例如。

1
self.str = [[[NSString alloc] init] autorelease];

子线程中的内存管理

前面提到了自动释放池 NSAutoReleasePool 对象。在主线程中,系统会自动创建一个自动释放池对象。在子线程中,则需要自己创建自动释放池对象,并在线程结束时释放自动释放池。否则,调用 autorelease 无效,导致内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
// self.thread为retain的属性
self.thread = [[[NSThread alloc] initWithTarget:self selector:@selector(newThreadFunc) object:nil] autorelease];

// ...

- (void)newThreadFunc
{
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init];

// jobs

[pool drain]; // or [pool release];
}

关于调用 drain 还是 release。在高版本 sdk 中,两者是等价的。但是低版本中,drain 才能释放 pool。因此,考虑到兼容性,建议调用 drain。

ARC (Automatic Reference Counting)

ARC,自动引用计数。前面讲述的内存管理,虽有 autorelease 帮助,但还是属于手动管理内存。ARC 是编译期技术。在 ARC 下,是不允许手动调用 retain / release / autorelease,完全交给编译器去管理。

⚠️ 注意:在 MRC 下,__strong 修饰变量,引用计数不会 +1,在 ARC 下引用计数才会 +1

循环引用

引用计数的内存管理方案,需要注意避免循环引用引发内存泄漏。
这里介绍几种容易引起循环引用并导致的内存泄漏的情况。解决思路无非两个,一是避免循环引用,而是在某个时机打破循环引用。

delegate 是 retain 属性

delegate 的设计模式在 iOS 开发中非常常见,一般情况下是 A 对象拥有 B 对象,B 对象中通过 delegate 引用 A,如果 delegate 是 retain 属性,就会造成循环引用。

而这种情况往往会造成大内存的泄漏,比如 A 是某个 ViewController。

解决方案:建议把 delegate 改成 weak 属性。(MRC 下也可以支持 weak,需要在 Build Settings 中将「CLANG_ENABLE_OBJC_WEAK」改为 Yes);或者把 delegate 改成 assign 属性,并在恰当的时机(一般是在 dealloc 中)把 delegate 赋值为 nil。

NSTimer 没有 invalidate

NSTimer 会强引用 target 对象。self 对象有一个 retain 保存 timer 同时 timer 的 target 就是 self,这种情况也很常见。

如果 timer 是单次(repeat = NO)的,timer 在执行会掉后会释放对 target 的引用。如果 timer 的 repeat = YES,那么 timer 就会一直持用对 target 的强引用,这个时候就需要找到某个时机主动调用 NSTimer 的 invalidate 方法,打破循环引用。

block 引起的循环引用

我们将一个引用了 self 的 block 赋值给 strong / copy 的属性时,就构成一个循环引用。例如:

1
2
3
4
5
- (void)doSomething {
self.block = ^{
[self print];
};
}

这时可以找个时机将 block 属性置为 nil,打破循环引用。或者在 block 赋值时使用 weakSelf,逻辑如下:

1
2
3
4
5
6
7
8
9
10
- (void)doSomething {
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// do something with strongSelf...
[strongSelf print];
}
};
}