iOS内存管理小结

引用计数

说到iOS的内存管理方式,相信开发过的人都能多多少少说出来一些,但是我发现它在我的脑子中还是没有一个很清晰的框架,所以花了些时间整理一下。Obj-C使用的是引用计数的方式来管理内存中的对象,对象的引用计数为0时会被销毁,大于0时会还停留在内存中。

如官方文档中的图所示:

(琢磨了好半天,觉得这张图的表示有点点问题,前面2个圆圈下面的数字表示的是经过上面箭头上的操作后得到的,但是后面的两个release的圆圈下的数字表示的却是当前的retain count,下一步的数字表示的才是上一步的结果,一个图里这样不统一,真的有点让人费解唉(´・_・`))

Obj-C提供了两种内存管理方式:

  • manual retain-releaseorMRR,就是我们自己来调用内存管理的方法管理对象的内存。
  • Automatic Reference CountingorARC,Xcode 4.2之后,编译器帮助我们来自动插入合适的内存管理方法了。

内存管理的基本规则

  • 自己生成的对象,自己持有

alloc/new/copy/mutableCopy开头的方法生成的对象意味着自己持有。类似于这样的方法命名allocMyObject/newThatObject/copyThis/mutableCopyYourObject,但是allocate/newer/copying/mutableCopyed不在此列,必须是以驼峰式的命名。

这些在ARCMRC时都是一样的,但是在ARC时多了一条命名规则init。它只返回实例对象,返回的对象不会注册到autoreleasepool中,基本上是对alloc返回值的对象进行一些初始化处理并返回该对象。

  • 非自己生成的对象,自己也能持有

[NSMutableArray array]这样生成的对象,并非是自己生成的对象,但是自己可以持有。

  • 自己持有的对象不再需要时释放
  • 非自己持有的对象无法释放

无论ARC有效还是无效时,都遵循这些规则,下面就分别从这两个方向介绍~

MRR内存管理

引用计数的实现规则

  • 当创建一个对象时,它的引用计数至少为1。不能准确的说,它的引用计数就是1,因为在init方法中可能还会被其他对象所引用。
  • 给一个对象发送retain消息时,它的引用计数加1。
  • 给一个对象发送release消息时,它的引用计数减1。给一个对象发送autorelease消息时,它的引用计数也会减1,但是会延迟到当前的autorelease pool结束时。
  • 如果一个对象的引用计数为0,调用dealloc方法销毁该对象。销毁是什么意思呢,就是它所在的那块内存会被标记为可用。

Property

当在对象的头文件中声明一个property时,编译器会自动生成该property对应的get/set方法。同时也会自动提供一个带了_的对应的实例变量,除非我们自己用synthesized声明了一个。假设我们有一个Counter对象,它有一个叫countproperty,如下:

1
2
3
@interface Counter : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;

那这个property对应的get/set会像如下实现:

1
2
3
4
5
6
7
8
9
- (NSNumber *)count {
return _count;
}

- (void)setCount:(NSNumber *)newCount {
[newCount retain]; //确保新值不会以外的被释放掉
[_count release]; //原来的值不需要时要释放掉
_count = newCount; //赋新的值
}

可以看到,自动生成的这一对get/set方法已经帮我们做了内存管理,在给property赋新值的时候,一定要记住使用该set方法,不仅方便,还会更加安全。假如我们要实现一个count的reset方法,可以像如下这样:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release]; //自己创建的所以要自己释放
}

//或者
- (void)reset {
NSNumber *zero = [NSNumber numberWithInteger:0];
[self setCount:zero];
//zero非自己创建的,所以不用释放
}

但是如果你是这样实现的:

1
2
3
4
5
- (void)reset {
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[_count release];
_count = zero; //直接调用实例变量
}

有可能你会忘记释放掉原来的对象,而且这样的方式在使用KVO时也是不起作用的。

但是有一点需要注意,不要在初始化方法中和dealloc方法中使用上面的get/set方法,要直接用实例变量!

集合

当向一个集合中添加对象时,在集合的addObject内部实现中会调用该对象的retain方法,来确保对象在集合中时不会被销毁掉。当一个对象从集合中移除掉时或者集合本身被销毁掉时,集合会向该对象发送release消息。所以你看,当把一个对象放到集合中时,集合会自己管理对其的引用和释放。通常可以用下面两种方式向集合中添加对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
[array addObject:convenienceNumber];
}

//或者
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
[array addObject:allocedNumber];
[allocedNumber release];
}

循环引用

说到引用计数管理内存的方式就不得不说一下循环引用,当A通过直接或间接的方式引用了B,B又同样的引用了A,以至于A、B都要等待对方释放掉时自己才能被释放,结果就是谁也不能被释放,这时内存泄露发生了。所谓内存泄露就是应当废弃的对象在超出其生命周期后继续存在。循环引用也有发生在自身对自身的强引用的时候。

那在MRR时怎样来避免循环引用的发生呢?答案很简单,A或B的一方在引用对方时,不要调用其retain方法就好啦,这样被引用的对象引用计数并不会改变。

ARC内存管理

时间来到了ARC时代,从Xcode 4.2开始默认就是开启ARC的了,同样的,要先搞清楚上面那几个基本问题。

引用计数的实现规则

不用手动调用内存管理的方法了,但是多了几个变量修饰符:

  • __strong 修饰符
  • __weak 修饰符
  • __unsafe_unretained 修饰符
  • __autoreleasing 修饰符

__strong

__strong顾名思义是对对象的强引用,通常我们创建的id类型和对象类型变量默认都是__strong修饰符,只是不需要显示的写出来。__strong修饰的变量会在离开作用域时,即该变量被废弃时,会释放其被赋予的对象。另外,__strong以及__weak``__autoreleasing可以保证将附有这些修饰符的变量初始化为nil。

1
2
3
4
5
6
7
8
id __strong obj0;
id __weak obj1;
id __autoreleasing obj2;

//下面的与上面的相同
id __strong obj0 = nil;
id __weak obj1 = nil;
id __autoreleasing obj2 = nil;

通过__strong我们不需要再键入retainrelease啦。

__weak

但是光有__strong还是不够的,循环引用的问题还是会发生,所以__weak出现了。用__weak修饰变量,明确的告诉编译器,这个变量对引用的对象是弱引用,不会持有对象。__weak还有一个优点,在持有某对象的弱引用时,若该对象被废弃,则此弱引用自动失效且处于nil被赋值的状态。

使用__weak可以避免循环引用,通过检查附有__weak修饰符的变量是否为nil,可以判断被赋值的对象是否已废弃。这一点可以在开发中好好利用一下~

__unsafe_unretained

这个修饰符我们在日常的开发中可能并不常用,因为__weak是在iOS5及以上的版本中才可用,那在之前就需要用到__unsafe_unretained。它的作用与__weak一样,都是不会强引用对象,但是它修饰的变量不属于编译器管理的内存对象。

啊哈,崩溃掉了!最后一行,obj1表示的变量在离开作用域后因为没有变量对其引用了,所以已经被废弃掉了,访问一个被废弃的对象,所以崩溃了。但是偶尔也可能会正常访问,因为被废弃的对象所占用的那块空间,可能系统还没有来的及清理或分配给其他对象。所以在使用__unsafe_retained修饰符时,赋值给附有__strong的变量时有必要确保被赋值的对象确实存在。

现在App应该都不会兼容iOS5之前的系统,所以我觉得忘掉__unsafe_unretained吧,有__weak即可,用着省心呐~

__autoreleasing

ARC有效时,autorelease一样可用,只是换了种形式:

1
2
3
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}

有效的利用好autoreleasepool能够使内存不至于在某个瞬间暴增,它是我想重点总结的,所以下面会用一块单独说,此处先留个印象ヽ(´▽`)/

Property

在ARC有效时,看一下声明的属性与所有权修饰符的对应关系:

属性声明的属性 所有权修饰符
assign __unsafe_unretained
copy __strong (但是赋值的是被复制的对象)
retain __strong
strong __strong
unsafe_unretained __unsafe_unretained
weak __weak

这里的copy注意,copy属性并不是简单的赋值,它赋值的是通过NSCopying接口的copyWithZone:方法复制赋值源所生成的对象。

autoreleasepool

给一个对象发送autorelease消息,会将该对象放到离其最近的自动释放池中,并延迟释放该对象。在ARC无效时,像下面这样使用:

1
2
3
4
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];

在[pool drain]时,会像当前池子中的每个对象发送release消息,同时也会调用[pool release],所以不需要再显示的调用一遍[pool release]了。在ARC有效时,写法如下:

1
2
3
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}

写法变得更加简洁了,当走出大括号标识的作用域后,池子中的对象就会被释放掉。其实也可以不显示指定__autorelease,alloc的对象在走出作用域后,强引用会失效,对象自动被释放。那还有必要将其放在autoreleasepool中吗?答案是有必要,因为init中可能产生大量的会延迟释放的对象。此外,不是以alloc/new/copy/mutableCopy开头的方法生成的对象,实际上是要延迟释放的对象,相当于ARC无效时,调用了autorelease的对象。

1
2
3
4
5
6
7
8
id obj = [NSMutableArray array]; //非自己生成但是持有的对象

//[NSMutableArray array]可以写成以下形式:
+ (id)array {
id obj = [[NSMutableArray alloc] init];
return obj;
}

执行到return时,obj超出了其作用域会被释放掉,但是它作为返回值不能马上消失,所以编译器自动把它放到autoreleasepool了。那到底是放到哪个autoreleasepool中了呢?挑最近的放~

通常我们不需要对每一个这样的autorelease对象都如此操心合适要释放它,实际上系统自动创建的线程例如主线程,或者GCD中的线程,在其对应的NSRunloop每次循环开始时会自动创建一个autoreleasepool,然后在循环结束时释放池子,同时释放掉在这一次循环中处理不同事件时产生的autorelease对象。

那么下一个问题就来了,如果在某一时间要创建大量的临时对象,比如笔者就遇到过一次从相册中读入大量图片,然后处理它们的尺寸等等,这时要是NSRunloop的一次循环还没结束,正在处理其他事件呢,那这些临时对象就无法释放了,导致内存急剧增加。这时就要自己给这些临时变量增加一个autoreleasepool了,如下:

1
2
3
4
5
6
7
8
for (int i = 0; i < 图像数; i++) {
@autoreleasepool {
/*
* 读入图像
* 大量产生的autorelease图像
* /
}
}

另外,在ARC无效时,NSAutoreleasePool是可以嵌套使用的:

1
2
3
4
5
6
7
8
NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool2 drain];
[pool1 drain];
[pool0 drain];

在ARC有效时,使用@autoreleasepool一样可以嵌套,程序的可读性可能更好一些。

1
2
3
4
5
6
7
@autoreleasepool {
@autoreleasepool {
@autoreleasepool {
id __autorelease obj = [[NSObject alloc] init];
}
}
}

还有一个地方你可能看到了@autoreleasepool,那就是很不起眼的main.m。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

当应用结束时,这个autoreleasepool块也就结束了,这是它要释放掉所有UIApplicationMain产生的autorelease的对象。也可以认为,这个autoreleasepool是包在其他所有pool外面的,这样在操作系统清理内存时,也不至于让有的autorelease对象没有pool归属。

小结

  • 理解好引用计数管理内存的基本原则。
  • 使用ARC会让内存管理在开发中变得容易很多。
  • 清楚什么时候会发生循环引用,使用弱引用来打破循环引用。
  • 在会产生大量autorelease对象的地方,使用Autorelease Pool来降低内存的开销。

最后

本文内容参考总结自以下出处:

先挖两个坑,后面分析一下autoreleasepool的内部实现,和retain/release的内部实现~

本文作者:意林
本文链接:http://shinancao.github.io/2017/03/12/iOS-Memory-Management/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!