iOS内存管理

简介

Objective-C中提供了两种内存管理机制:MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分别提供对内存的手动和自动管理,来满足不同的需求。iOS 5之前普遍使用MRC来管理内存。iOS 5之后,苹果推出ARC。现在普遍时候ARC来管理内存。

iOS内存分区

iOS的内存分区跟C语言类似:

  • 栈区(stack):存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;程序猿不需要管理栈区变量的内存;栈区地址从高到低分配。

  • 堆区(heap):堆区的内存分配使用的是alloc;需要程序猿管理内存;ARC的内存的管理,是编译器再编译的时候自动添加retain、release、autorelease;堆区的地址是从低到高分配。

  • 全局区/静态区(static):包括两个部分:未初始化过 、初始化过;也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;eg:int a;未初始化的。int a = 10;已初始化的。

  • 常量区:常量字符串就是放在这里。

  • 代码区:存放App二进制代码。

引用计数

在谈ARC和MRC之前我们先来谈谈引用计数

引用计数(Reference Count)是一个简单而有效的管理对象生命周期的方式。当我们创建一个新对象的时候,它的引用计数为 1,当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象是,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。由于引用计数简单有效,除了 Objective-C 和 Swift 语言外,微软的 COM(Component Object Model )、C++11(C++11 提供了基于引用计数的智能指针 share_prt)等语言也提供了基于引用计数的内存管理方式。

MRC

MRC即我们通过人为的方式来控制引用计数器的增减,影响对象RetainCount值得方法有以下几种:

  1. new、alloc、copy、mutableCopy,这几种方法用来创建一个新的对象并且获得对象的所有权,此时RC的值默认为RetainCount=1;
  2. retain,对象调用retain方法,该对象的RetainCount+1;
  3. release,对象调用 release方法,该对象的RetainCount-1;
  4. dealloc,dealloc方法并不会影响RetainCount的值,但是当RetainCount的值为0时,系统会调用dealloc方法来销毁对象。

在MRC模式下,如果一个对象被释放,它的RetainCount并不是立即变为0的
例如:

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSObject *object = [[NSObject alloc] init];
NSLog(@"Reference Count = %u", [object retainCount]);
[object release];
NSLog(@"Reference Count = %u", [object retainCount]);
return YES;
}

输出结果:

1
2
Reference Count = 1
Reference Count = 1

我们注意到,最后一次输出,引用计数并没有变成 0。这是为什么呢?因为该对象的内存已经被回收,而我们向一个已经被回收的对象发了一个 retainCount 消息,所以它的输出结果应该是不确定的,如果该对象所占的内存被复用了,那么就有可能造成程序异常崩溃。
那为什么在这个对象被回收之后,这个不确定的值是 1 而不是 0 呢?这是因为当最后一次执行 release 时,系统知道马上就要回收内存了,就没有必要再将 retainCount 减 1 了,因为不管减不减 1,该对象都肯定会被回收,而对象被回收后,它的所有的内存区域,包括 retainCount 值也变得没有意义。不将这个值从 1 变成 0,可以减少一次内存的写操作,加速对象的回收。

MRC下内存管理有是个法则

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不在需要自己持有对象的时候,释放。
  • 非自己持有的对象无需释放。

ARC

ARC 是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码以及在 Runtime 做一些优化。

ARC模式下,创建的新对象通常由以下几种关键字来限定

  1. strong(默认值),由strong修饰的为强指针,对象只要有强指针指向就不会被销毁;每当一个强指针指向一个对象,该对象的的RC+1;
  2. weak,由weak修饰的为弱指针,弱指针所指向的对象并不会改变RC值,弱指针只表示是对对象的引用;当弱指针所指向的对象销毁时,该弱指针的值变为nil;
  3. unsafe_unretained,unsafe_unretained修饰的对象指针所指向的对象也不会改变RC值,也只表示是对对象的引用;当所指向的对象销毁时,该指针的值不会变为nil,仍是保留原有的地址

在ARC模式下,MRC中的retain、release等方法变的不可用,因为ARC是不需要我们手动管理内存的,一切由编译器完成。

ARC虽然是自动管理引用计数,但是如果使用不当也会造成内存泄漏。
ARC下有以下几个内存泄漏的点:

  • Delegate
  • Block
  • NSTimer
  • CoreFundation/CoreGraphics
  • performSelector

Xcode 的 Instruments 工具集可以很方便的检测循环引用

属性关键字

strong :强引用,ARC中使用,与MRC中retain类似,使用之后,计数器+1。
weak :弱引用 ,ARC中使用,如果只想的对象被释放了,其指向nil,可以有效的避免野指针,其引用计数为1。
readwrite : 可读可写特性,需要生成getter方法和setter方法时使用。
readonly : 只读特性,只会生成getter方法 不会生成setter方法,不希望属性在类外改变。
assign :赋值特性,不涉及引用计数,弱引用,setter方法将传入参数赋值给实例变量,仅设置变量时使用。
retain :表示持有特性,setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
copy :表示拷贝特性,setter方法将传入对象复制一份,需要完全一份新的变量时。
nonatomic :非原子操作,不加同步,多线程访问可提高性能,但是线程不安全的。决定编译器生成的setter getter是否是原子操作。
atomic :原子操作,同步的,表示多线程安全,与nonatomic相反。

ARC下默认关键字

  • 对应基本数据类型默认关键字是
    atomic,readwrite,assign
  • 对于普通的 Objective-C 对象
    atomic,readwrite,strong

既然说到了@property就说说@synthesize和@dynamic

@property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;

@synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。

@dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

ARC和MRC混编

  1. 在targets的build phases选项下Compile Sources下选择,不使用arc编译的文件,双击它,输入 -fno-objc-arc 即可(这个类就可以使用MRC模式)

  2. MRC工程中也可以使用ARC的类。方法如下:
    在targets的build phases选项下Compile Sources下选择要使用arc编译的文件,双击它,输入 -fobjc-arc 即可

Autorelease Pool

Autorelase Pool 提供了一种可以允许你向一个对象延迟发送release消息的机制。当你想放弃一个对象的所有权,同时又不希望这个对象立即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的作用就显现出来了。
谓的延迟发送release消息指的是,当我们把一个对象标记为autorelease时.这个对象的 retainCount 会+1,但是并不会发生 release。当这段语句所处的 autoreleasepool 进行 drain 操作时,所有标记了 autorelease 的对象的 retainCount 会被 -1。即 release 消息的发送被延迟到 pool 释放的时候了。
在ARC 环境下,苹果引入了 @autoreleasepool 语法,不再需要手动调用 autorelease 和 drain 等方法

autoreleasepool 以一个队列数组的形式实现,主要通过下列三个函数完成.

  • objc_autoreleasepoolPush
  • objc_autoreleasepoolPop
  • objc_autorelease
    看函数名就可以知道,对 autorelease 分别执行 push,和 pop 操作。销毁对象时执行release操作。

Autorelease Pool 的用处

在 ARC 下,我们并不需要手动调用 autorelease 有关的方法,甚至可以完全不知道 autorelease 的存在,就可以正确管理好内存。因为 Cocoa Touch 的 Runloop 中,每个 runloop circle 中系统都自动加入了 Autorelease Pool 的创建和释放。

当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool 可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool 会在整个 runloop circle 结束之后才进行 drain,手动创建的话,会在 block 结束之后就进行 drain 操作。详情请参考苹果官方文档。一个普遍被使用的例子如下

1
2
3
4
5
6
7
8
for (int i = 0; i < 100000000; i++)
{
@autoreleasepool
{
NSString* string = @"ab c";
NSArray* array = [string componentsSeparatedByString:string];
}
}

如果不使用 autoreleasepool ,需要在循环结束之后释放 100000000 个字符串,如果 使用的话,则会在每次循环结束的时候都进行 release 操作。

main.m 中 Autorelease Pool 的解释

大家都知道在 iOS 程序的 main.m 文件中有类似这样的语句:

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

根据苹果官方文档, UIApplicationMain 函数是整个 app 的入口,用来创建 application 对象(单例)和 application delegate。尽管这个函数有返回值,但是实际上却永远不会返回,当按下 Home 键时,app 只是被切换到了后台状态。

同时参考苹果关于 Lifecycle 的官方文档,UIApplication 自己会创建一个 main run loop,我们大致可以得到下面的结论:

  1. main.m 中的 UIApplicationMain 永远不会返回,只有在系统 kill 掉整个 app 时,系统会把应用占用的内存全部释放出来。
  2. 因为(1), UIApplicationMain 永远不会返回,这里的 autorelease pool 也就永远不会进入到释放那个阶段
  3. 在 (2) 的基础上,假设有些变量真的进入了 main.m 里面这个 pool(没有被更内层的 pool 捕获),那么这些变量实际上就是被泄露的。这个 autorelease pool 等于是把这种泄露情况给隐藏起来了。
  4. UIApplication 自己会创建 main run loop,在 Cocoa 的 runloop 中实际上也是自动包含 autorelease pool 的,因此 main.m 当中的 pool 可以认为是没有必要的

基于 AppKit 框架的 Mac OS 开发中, main.m 当中就是不存在 autorelease pool 的,也进一步验证了我们得到的结论。不过因为我们看不到更底层的代码,加上苹果的文档中不建议修改 main.m ,所以我们也没有理由就直接把它删掉(亲测,删掉之后不影响 App 运行,用 Instruments 也看不到泄露)。

以上就是对内存管理的一些总结。

参考

理解 iOS 的内存管理
Objective-C内存管理