iOS内存管理

前言

Objective-C是一个面向对象的语言,所以内存管理显得尤为重要。不过在Objective-C中,我们只需要管理对象的内存,非对象的内存不需要我们关心,比如char int 等类型的数据是放置在栈上的,交由系统自动回收。Objective-C一直是使用引用计数来管理对象的内存。什么是引用计数?简单来说,每个对象都有一个计数器,用以表示当前有多少个事物想令此对象继续存活下去,也叫做保留计数。当该对象的计数为0时,该对象就可以废弃了。

在iOS5之前,如果你编写iOS应用程序,你需要手动去管理你创建的对象的内存,即MRC(Manual Reference Counting)。iOS5引入了自动引用计数,即ARC(Automatic Reference Counting)

ARC的出现,将内存管理这个活从开发者本身转移到了编译器上面。LLVM引入了ARC机制后,可以很清楚目标对象,并能立刻释放那些不再被使用的对象,这样不仅大大减少了开发者的工作量,还使得程序本身的稳定性得到很好的提升。开发者可以更加专注于业务逻辑而不是内存管理。

引用计数

MRC时代,Objective-C就使用引用计数来管理对象的内存。NSObject协议声明了三个方法用于操作计数器,以递增或递减其值:

  • retain 递增
  • release 递减
  • autorelease 递减

举个简单的列子:

1
2
3
4
if ([[self canLog]]) {
NSString *message = [[NSString alloc] initWithString:@"log"];
NSLog(@"%@",message);
}

MRC下 这段代码存在内存泄漏的问题,因为if语句块末尾并没有手动释放message对象。需要对被释放的对象调用release方法,使其引用计数减1。

1
2
3
4
5
if ([[self canLog]]) {
NSString *message = [[NSString alloc] initWithString:@"log"];
NSLog(@"%@",message);
[message release];//ARC下 编译器会自动完成
}

ARC下则没有问题,因为ARC下,编译器会自动为你添加保留与释放操作。所以,直接在ARC下调用retain release autorelease 等内存管理方法是不能通过编译的,因为手工调用的话,会让干扰ARC判断何处应该自动调用内存管理方法。

实际上,ARC在调用这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层C语音版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。比方说,ARC会调用与retain等价的底层函数objc_retain。这也是不能覆写retain release等方法的原因。

所以对于保留计数的概念应该这么来理解:绝不应该说保留计数一定是某个值,只能说你所执行的操作是递增了还是递减了该计数。

引用计数是如何管理内存的

我们想一下,编写程序的时候,我们对内存管理的思考方式应该是什么:

  1. 自己生成的对象,自己持有
  2. 非自己生成的对象,自己也能持有
  3. 不再需要自己持有的对象时释放
  4. 非自己持有的对象无法释放

理解好以上4点,对于内存是如何通过引用计数进行管理的就很容易明白了。
这4点里我们提到了3个词很重要:生成 持有 释放。对于Objective-C的内存管理来说还要加上一个词:废弃。这四个词在Objective-C中的对应方法如下:

  1. 生成:alloc new copy mutableCopy
  2. 持有:retain
  3. 释放:release
  4. 废弃:dealloc

这些方法属于Cocoa框架Foundation框架类库中的NSObject类的方法,适用于OS X和iOS应用开发。

那什么叫自己生成的对象和非自己生成的对象?
这里可不是简单的指你编写的代码和别人编写的代码生成的对象之分。而是调用的方法之分。通过alloc new copy mutableCopy等方法或者是使用这些名称开头的方法生成的对象称为自己生成的对象。而使用这些方法之外创建的对象(类似于[NSArray arrry]这种类方法创建的对象)称为非自己生成的对象。

MRC下的内存管理

  1. 自己生成的对象,自己持有
    1
    2
    3
    4
    5
    //自己生成并持有对象
    id obj = [[NSObject alloc]init];
    id obj2 = [NSObject new];
    id obj3 = [obj copy];
    id obj4 = [obj3 mutableCopy];

copymutableCopy的不同在于,copy方法生成并持有不可变的对象副本,mutableCopy生成并持有可变对象的副本。用这两个方法生成的对象,虽然是对象的副本,但同alloc new等方法一样,在自己生成并持有对象这点上是一样的。

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

    1
    2
    3
    4
    //取得对象的存在 但自己并不持有
    id obj = [NSArray array];
    //自己持有对象
    [obj retain];
  2. 不再需要自己持有的对象时释放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //自己生成并持有对象
    id obj = [[NSObject alloc]init];
    //释放对象
    [obj release];
    //取得对象的存在 但自己并不持有
    id obj = [NSArray array];
    //自己持有对象
    [obj retail];
    //释放对象
    [obj release];
  3. 非自己持有的对象无法释放

    1
    2
    3
    4
    //取得对象的存在 但自己并不持有
    id obj = [NSArray array];
    //释放了非自己持有的对象会导致程序崩溃
    [obj release];

ARC下的内存管理

所有权修饰符

我们知道Objective-C中处理对象,需要将变量类型定义为id类型或各种对象类型。这里要注意的是,事实上并没有对象变量这样的东西存在。它仅仅是一个引用到变量的对象,是一个指针,是一个地址,它并不是一个对象的容器里面装载了对应的对象。比如在Java中,(java是非常注重对象类型的),我们不会也不应该知道引用变量中装载的是什么,它只是用来代表单一的对象(注意 是代表 而不是本身是),只有java虚拟机才会知道如何使用引用来取得该对象。回到Objective-C中,,所谓的对象类型,其实就是指向NSObject的指针,例如NSObject *或者id等(id是万能指针,它可以指向任何类型的对象,你可以理解为它是一个可以修饰任何类型的对象引用,相当于C语音中的void *
那么在ARC的机制下,所有的对象类型和id类型必须要加上一个东西,就是刚才我们提到的所有权修饰符。
所有权修饰符一共分为4种:__strong __weak __unsafe_unretained __autoreleasing

__strong

__strong 修饰符表示对对象的强引用,保留了此值。是所有对象类型和id类型的默认修饰符。以下代码是等同关系:

1
2
id obj1 = [[NSObject alloc] init];
id __strong obj1 = [[NSObject alloc] init];

__strong 修饰符会产生循环引用(比如A强引用B的同时B也强引用A,那么A和B永远都不会被销毁,因为彼此强引用着对方,任何一方的强引用失效都只能基于对方的强引用失效,这样就产生了死循环。类似于死锁的问题),这也是引用计数式内存管理必然会产生的问题,为了解决这个问题,所以引入了下面的__weak

__weak

__weak 修饰符表示对对象的弱引用,不保留此值。使用__weak 修饰符可以避免循环引用(比如让A和B中的任意一方将其强引用改成弱引用或者都改成互相弱引用)。

__unsafe_unretained

__unsafe_unretained修饰符和_weak修饰符一样,表示对对象的弱引用,不保留此值。是一个不安全的所有权修饰符。尽管ARC下的内存管理是编译器的工作,但附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,所以会造成不安全的情况。

__autoreleasing

要说__autoreleasing所有权修饰符。我们需要先了解自动释放池这个概念。
自动释放池是iOS引用计数架构中的一项重要特性。我们知道调用release会立刻递减对象的保留计数。然而有的时候我们可以不调用release,改为调用autorelease,会将对象加入到对应的自动释放池中,此方法会在稍后递减(通常是在下一次事件循环时递减)。

举个简单的列子:
当我们需要一个方法提供的返回对象时,autorelease就非常有用了。看一下这个方法:
MRC

1
2
3
4
-(NSString *)stringValue{
NSString *string = [[NSString alloc]initWithFormat:@"i am a %@",self];
return string;
}

这里的string的保留计数比期望值要多1,因为我们进行了alloc操作,但是我们又没有对应的释放操作。这就意味着调用者要负责处理多出来的这一次保留计数,必须设法将其抵消。但是我们又不能再方法里直接释放,否则,return的就是一个空值了。这时候autorelease就非常有用了。它会在稍后释放改对象,从而给调用者留出了足够多的时间,使其在需要的时候保留返回值。

1
2
3
4
-(NSString *)stringValue{
NSString *string = [[NSString alloc]initWithFormat:@"i am a %@",self];
return [string autorelease];
}

这样一来的话,由于返回的string会在稍后自动将其保留计数减一,调用者就无需再对其进行内存管理了。不过在ARC下,这个并不需要开发者去完成,编译器会为我们搞定的。

ARC下,我们很少显式的调用__autoreleasing 比如

1
2
3
4
@autoreleasepool {
id obj = [NSMutableArray array];
//编译器会自动检查方法名,如果不是alloc/new/copy/mutableCopy开始的方法,则自动将返回值对象注册到autoreleasepool
}

再比如ARC下,string作为局部变量的函数返回值,编译器也会自动将其注册到autoreleasepool 中的。

1
2
3
4
-(NSString *)stringValue{
NSString *string = [[NSString alloc]initWithFormat:@"i am a %@",self];
return string;
}

再比如__weak修饰符。虽然__weak修饰符是为了避免循环引用而使用的,但在访问__weak修饰符的变量时,实际上必定要访问注册到autoreleasepool 的对象。以下两个代码是等同的。

1
2
3
4
5
6
id __weak obj = obj2;
NSLog(@"class=%@",[obj Class]);

id __weak obj = obj2;
id __autoreleasing temp = obj;
NSLog(@"%@class=%@",[temp class]);

为什么在访问持有__weak修饰符的变量时必须访问注册到autoreleasepool的对象呢?这是因为__weak修饰符只持有对象的弱引用,而在访问对象的过程中,改对象有可能被废弃,这样就可能会造成内存泄漏。此时把要访问的对象注册到autoreleasepool中,那么在 @autoreleasepool结束之前,都能确保改对象存在。因此,在使用__weak修饰符的变量时就必定要使用注册到autoreleasepool中的对象,不过这些在ARC下都有编译器自动完成,不需要我们进行管理。

自动释放池

现在我们来详细了解一下自动释放池(autoreleasePool)。自动释放池机制类似于“栈”,系统创建好自动释放池后将其推入栈中。而清空释放池,就相当于将其从栈中弹出。在对象上执行自动释放操作,相当于将其放入栈顶的那个池中。通常情况下,我们无需担心自动释放池的创建问题。iOS应用程序是在Cocoa Touch环境下运行的,系统会自动创建一些线程,比如主线程。这些线程默认都有自动释放池,每次执行“事件循环”时,就将其清空。因此,不需要自己来创建自动释放池。通常只有一个地方需要创建自动释放池,那就是main函数,我们用自动释放池来包裹应用程序的主入口点。一般iOS程序的main函数:

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

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

从技术角度看,这里不是非得有个自动释放池才可以。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果这里不弄一个自动释放池的话,UIApplicationMain函数所释放的那些对象就没有自动释放池可以容纳了,所以说,这个池可以理解成最外围捕捉全部自动释放池对象所用的池。这里就又引入了一个知识点,自动循环池是可以嵌套的。举个例子:

1
2
3
4
5
6
@autoreleasepool {
NSString *string = [[NSString alloc]initWithFormat:@"1=%i",1];
@autoreleasepool {
NSNumber *number = [NSNumber numberWithInt:1];
}
}

将自动释放池嵌套使用的好处是,可以借此控制应用程序的内存峰值,使其不至于过高。看下面这段代码:

1
2
3
for (int i = 0; i<10000; i++) {
[self doSomethingWithInt:i];
}

如果doSomethingWithInt方法要创建一些临时对象,那么这些对象很可能会放在自动释放池里,等待系统稍后将其释放并回收。但是自动释放池要等待程序执行下一个事件循环时才清空。这就意味着在执行for循环时,会持续的有新对象创建出来,并加入自动释放池,这些对象都要等待for循环执行完才会释放。这样一来,在执行for循环的时候,应用程序所占的内存就会持续上涨。而等到所有的临时对象都释放后,内存又会突然下降。
通过嵌套使用自动释放池可以很好的解决这个问题,我们将循环内的代码包裹在自动释放池中:

1
2
3
4
5
for (int i = 0; i<10000; i++) {
@autoreleasepool{
[self doSomethingWithInt:i];
}
}

这样每次for循环创建出的那些临时对象,在用完之后就不用放到线程的主释放池里等待整个for循环结束后释放,而是每次for循环创建的临时对象都会放到对应for循环创建出来的释放池中,等到当前for循环结束后释放。这样内存峰值就会降低了。
这里要注意的一点时,创建自动释放池也是有开销的,所以尽量不要额外的创建自动释放池。

在MRC中,创建自动释放池需要NSAutoreleasePool类。这个类专门用来表示自动释放池。这个了解一下即可,毕竟MRC已经是过去了。

1
2
3
NSAutoreleasePool *pool  = [NSAutoreleasePool alloc] init];
/**比较消耗内存的操作**/
[pool drain];

参考文献: 《Objective-C高级编程》 《Effective Objective-C 2.0》

0%