iOS之从MRC到ARC内存管理详解

3,701 阅读20分钟
原文链接: www.jianshu.com

概述

在iOS中开发中,我们或多或少都听说过内存管理。iOS的内存管理一般指的是OC对象的内存管理,因为OC对象分配在堆内存,堆内存需要程序员自己去动态分配和回收;基础数据类型(非OC对象)则分配在栈内存中,超过作用域就会由系统检测回收。如果我们在开发过程中,对内存管理得不到位,就有可能造成内存泄露。

我们通常讲的内存管理,实际上从发展的角度来说,分为两个阶段:MRC和ARC。MRC指的是手动内存管理,在开发过程中需要开发者手动去编写内存管理的代码;ARC指的是自动内存管理,在此内存管理模式下由LLVM编译器和OC运行时库生成相应内存管理的代码。

通篇主要介绍关于内存管理的原理及ARC和MRC环境下编写代码实现的差异。

提纲

一 引用计数
二 MRC操作对象的方法
  1. alloc/new/copy/mutableCopy
  2. retain
  3. release
  4. autorelease
  5. autorelease pool
三 ARC操作对象的修饰符
  1. __strong
    1.1 strong与变量
    1.2 strong与属性
    1.3 strong的实现
  2. __weak
    2.1 weak和循环引用
    2.2 weak和变量
    2.3 weak的实现
      2.3.1 weak和赋值
      2.3.2 weak和访问
  3. __unsafe_unretained
  4. _autoreleasing

具体介绍

一、引用计数

在OC中,使用引用计数来进行内存管理。每个对象都有一个与其相对应的引用计数器,当持有一个对象,这个对象的引用计数就会递增;当这个对象的某个持有被释放,这个对象的引用计数就会递减。当这个对象的引用计数变为0,那么这个对象就会被系统回收。

当一个对象使用完没有释放,此时其引用计数永远大于1。该对象就会一直占用其分配在堆内存的空间,就会导致内存泄露。内存泄露到一定程度有可能导致内存溢出,进而导致程序崩溃。

二、MRC操作对象的方法

1.alloc/new/copy/mutableCopy

1.1 持有调用者自己的对象

在苹果规定中,使用alloc/new/copy/mutableCopy创建返回的对象归调用者所有,例如以下MRC代码:

    NSMutableArray *array = [[NSMutableArray alloc] init];/*NSMutableArray类对象A*/
    
    NSLog(@"%p", array);
    
    [array release];//释放

由于对象A由alloc生成,符合苹果规定,指针变量array指向并持有对象A,引用计数器会加1。另外,array在使用完对象A后需要对其进行释放。当调用release后,释放了其对对象A的引用,计数器减1。对象A此时引用计数值为零,所以对象A被回收。不能访问已经被回收的对象,会发生崩溃。

1.2 持有非调用者拥有的对象

当持有非调用者自己拥有的对象的时候,例如以下代码:

    id obj = [Person person];
    
    [obj retain];
    
    /*do something*/
    
    [obj release];

此时obj变量获得但不持有Person类对象,可以通过retain进行持有该对象。当我们使用完该对象,应该调用release方法释放该对象。

注意:按照苹果的命名规则,必须是alloc/new/copy/mutableCopy开头,并且是符合驼峰命名规则生成的对象才归调用者所有。例如以下的方法,生成的对象不归调用者所有:

- (id)newarray;
- (id)allocwithInfo;
- (id)coPySomething;
- (id)mutablecopyItem;

2.retain

2.1 retain和属性

我们可以通过属性来保存对象,如果一个属性为强引用,我们就可以通过属性的实例变量和存取方法来对某个对象进行操作,例如某个属性的setter方法如下:

- (void)setPerson:(Person *)person {

    [person retain];
    
    [_person release];
    
    _person = person;
    
}

我们通过retain新值,release旧值,再给实例变量更新值。需要注意的一点是:需要先retain新值,再release新值。因为如果新旧值是同一个对象的话,先release就有可能导致该对象被系统回收,再去retain就没有任何意义了。例如下面这个例子:

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong)Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    //实例变量持有Person类对象(P对象)。这样赋值不会调用set方法
    _person = [[Person alloc] init];
    
    self.person = _person;//调用set方法
    
}

- (void)setPerson:(Person *)person {
    //release释放对P对象的引用,P对象引用计数值变为零,则P对象被系统回收
    [_person release];

    //由于P对象已经被回收,再去retain就容易出问题
    [person retain];
    
    _person = person;
    
}

@end

由于P对象被回收,对应其所分配的内存被置于“可用内存池”中。如果该内存未被覆写,那么P对象依然有效;如果内存被覆写,那么实例变量_person就会指向一个被覆写的未知对象的指针,那么实例变量就变成一个“悬挂指针”。

2.2 retain和数组

如果我们把一个对象加入到一个数组中,那么该数组的addObject方法会对该对象调用retain方法。例如以下代码:

    //person获得并持有P对象,P对象引用计数为1
    Person *person = [[Person alloc] init];//Person类对象生成的P对象
    
    NSMutableArray *array = [NSMutableArray array];
    
    //person被加入到数组,对象P引用计数值为2
    [array addObject:person];

此时,对象P被person和array两个变量同时持有。

3.release

3.1 自己持有的对象自己释放

当我们持有一个对象,如果在不需要继续使用该对象,我们需要对其进行释放(release)。例如以下代码:

    //array获得并持有NSArray类对象
    NSArray *array = [[NSArray alloc] init];
    
    /*当不再需要使用该对象时,需要释放*/
    [array release];
    
    //obj获得但不持有该对象
    id obj = [NSArray array];

    //持有对象
    [obj retain];    

    /*当不在需要使用该对象时,需要释放*/
    [obj release];
3.2 非自己持有的对象不要释放

当我们不持有某个对象,却对该对象进行释放,应用程序就会崩溃。

    //获得并持有A对象
    Person *p = [[Person alloc] init];//Person类对象A
    
    //p释放对象A的强引用,对象A所有者不存在
    //对象A引用计数为零,所以对象A被回收
    [p release];

    [p release];//释放非自己持有的对象

另外,我们也不能访问某个已经被释放的对象,该对象所占的堆空间如果被覆写就会发生崩溃的情况。

4.autorelease

autorelease指的是自动释放,当一个对象收到autorelease的时候,该对象就会被注册到当前处于栈顶的自动释放池(autorelease pool)。如果没有主动生成自动释放池,则当前自动释放池对应的是主运行循环的自动释放池。在当前线程的RunLoop进入休眠前,就会对被注册到该自动释放池的所有对象进行一次release操作。

autorelease和release的区别是:release是马上释放对某个对象的强引用;autorelease是延迟释放某个对象的生命周期。

autorelease通常运用在当调用某个方法需要返回对象的情况下,例如以下代码:

    //外部调用
    Person *p = [Person person];
    NSLog(@"%p", p);//使用无须retain

    //持有则需要retain
    [p retain];
    _person = p;
    [_person release];

    //Person类内部定义
+ (id)person {

    //创建的Person类对象由person获得并持有
    Person *person = [[Person alloc] init];
   
    //[person release];

    [person autorelease];
    
    return person;
}

在外部调用,从方法名person知道,创建的对象由p指针变量获得但不持有。在函数内部,person获得并持有了Person类对象,所返回的person对象的引用计数加1。换句话说,调用者需要额外处理这多出来的一个持有操作。另外,我们不能在函数内部调用release,不然对象还没返回就已经被系统回收。这时候使用autorelease就能很好地解决这个问题。

只要把要返回的对象调用autorelease方法,注册到自动释放池就能延长person对象的生命周期,使其在autorelease pool销毁(drain)前依然能够存活。

另外,person对象在返回时调用了autorelease方法。该对象已经在自动释放池中,我们可以直接使用对象p,无须再通过[p retain]访问;不过,如果要用实例变量持有该对象,则需要对变量p进行一次retain操作,示例变量使用完该对象需要释放该对象。

5. autorelease pool

5.1 autorelease pool和RunLoop(运行循环)

每条线程都包含一个与其对应的自动释放池,当某条线程被终止的时候,对应该线程的自动释放池会被销毁。同时,处于该自动释放池的对象将会进行一次release操作。

当应用程序启动,系统默认会开启一条线程,该线程就是“主线程”。主线程也有一个与之对应的自动释放池,例如我们常见的ARC下的main.h文件:

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

该自动释放池用来释放在主线程下注册到该自动释放池的对象。需要注意的是,当我们开启一条子线程,并且在该线程开启RunLoop的时候,需要为其增加一个autorelease pool,这样有助于保证内存的安全。

5.2 autorelease pool和降低内存峰值

当我们执行一些复杂的操作,特别是如果这些复杂的操作要被循环执行,那么中间会免不了会产生一些临时变量。当被加到主线程自动释放池的对象越来越来多,却没有得到及时释放,就会导致内存溢出。这个时候,我们可以手动添加自动释放池来解决这个问题。如以下例子所示:

for (int i = 0; i < largeNumber; i++) {
        
        //创建自动释放池
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        
        //产生许多被注册到自动释放池的临时对象
        id obj = [Person personWithComplexOperation];
        
        //释放池中对象
        [pool drain];
        
    }

如上述例子所示,我们执行的循环次数是一个非常大的数字。并且调用personWithComplexOperation方法的过程中会产生许多临时对象,所产生的临时对象有可能会被注册到自动释放池中。我们通过手动生成一个自动释放池,并且在每次循环结束前把该自动释放池的对象执行release操作释放掉,这样就能有效地降低内存的峰值了。

三 ARC操作对象的修饰符

1. __strong

1.1 __strong与变量

在ARC模式下,id类型和OC对象的所有权修饰符默认是__strong。当一个变量通过__strong修饰符来修饰,当该变量超出其所在作用域后,该变量就会被废弃。同时,赋值给该变量的对象也会被释放。例如:

{
    //变量p持有Person对象的强引用
    Person *p = [Person person];
    
    //__strong修饰符可以省略
    //Person __strong *p = [Person person];
}
    //变量p超出作用域,释放对Person类对象的强引用
    //Person类对象持有者不存在,该对象被释放

上述例子对应的MRC代码如下:

{
    Person *p = [Person person];
    
    [p retain];
    
    [p release];
}

可以看出,ARC和MRC是对应的。只不过在ARC下,对象“持有”和“释放”的内存管理代码交由系统去调用罢了。

1.2 strong与属性

如果一个属性的修饰符是strong,对于其实例变量所持有的对象,编译器会在该实例变量所属类的dealloc方法为其添加释放对象的方法。在MRC下,我们的dealloc方法有如:

- (void)dealloc {

    [p release];//ARC无效
    
    [super dealloc];//ARC无效
}

在ARC下,我们就无须再去写这样的代码了。另外,在dealloc方法中,ARC只能帮我们处理OC对象。如果实例变量持有类似CoreFoundation等非OC对象,则需要我们手动回收:

- (void)dealloc {

    CFRelease(_cfObject);
}

在ARC下,dealloc方法一般用来执行两个任务。第一个就是手动释放非OC对象;第二个是接触监听。另外,在ARC下我们不能主动调用dealloc方法。因为一旦调用dealloc,对象就不再有效。该方法运行期系统会在合适的时机自动去调用。

1.3 strong的实现

在ARC中,除了会自动调用“保留”和“释放”方法外,还进行了优化。例如某个对象执行了多次“保留”和“释放”,那么ARC针对特殊情况有可能会将该对象的“保留”和“释放”成对地移除。例如:

+ (id)person {

    Person *tmp = [[Person alloc] init];//引用计数为1
    [tmp autorelease];//注册到自动释放池(ARC无效)
    return tmp;
}

{
    //ARC
    _p = [Person person];//_p是强引用属性对应的实例变量
    
    //实现展示
    Person *p = [Person person];//Person类对象引用计数为1
    _p = [p retain];//递增为2
    [_p release];//递减为1
}
    //清空自动释放池,Person类对象递减为0,释放该对象

在上述代码中,ARC对应MRC的具体实现展示。在上面的展示中,+(id)person方法内部会调用一次autorelease操作来延迟其返回对象的生命周期,并且稍后在自动释放池进行release操作。_p通过retain来持有该对象,使用完就执行release操作。从而看出retain和autorelease是多余的,完全可以简化成以下代码:

+ (id)person {

    Person *tmp = [[Person alloc] init];//引用计数为1
    return tmp;
}

{
    //ARC
    _p = [Person person];//_p是强引用属性对应的实例变量
    
    //实现展示
    _p = [Person person];//Person类对象引用计数为1
    [_p release];//递减为0,Person类对象被回收
}

那么ARC是如何判断是否移除这种成对的操作呢?其实在ARC中,并不是直接执行retain和autorelease操作的,而是通过以下两个方法:

    objc_autoreleaseReturnValue(obj);//对应autorelease
    objc_retainAutoreleasedReturnValue(obj);//对应retain

以下为两个方法对应的伪代码:

id objc_autoreleaseReturnValue(id obj) {

    if ("返回对象obj后面的那段代码是否执行retain") {
        //是
        set_flag(obj);//设置标志位
        return obj;
    } else {
        return [obj autorelease];
    }
}

id objc_retainAutoreleasedReturnValue(obj) {

    if (get_flag(obj)) {
        //有标志位
        return obj;
    } else {
        return [obj retain];
    }
}

通过以上两段伪代码,我们重新梳理一下代码:

+ (id)person {

    Person *tmp = [[Person alloc] init];//引用计数为1
    
    return objc_autoreleaseReturnValue(id tmp);
}

{
    //ARC
    _p = [Person person];//_p是强引用属性对应的实例变量
    
    //实现展示
    Person *p = [Person person];//Person类对象引用计数为1
    _p = objc_retainAutoreleasedReturnValue(p);//递增为1
    [_p release];//递减为0
}

从上述示例代码可以看出:

  1. 当我们用变量p获取person方法返回的对象前,person方法内部会执行objc_autoreleaseReturnValue方法。该方法会检测返回对象之后即将执行的那段代码,如果那段代码要向所返回的对象执行retain方法,则为该对象设置一个全局数据结构中的标志位,并把对象直接返回。反之,在返回之前把该对象注册到自动释放池。

  2. 当我们对Person类对象执行retain操作的时候,会执行objc_retainAutoreleasedReturnValue方法。该方法会检测对应的对象是否已经设置过标志位,如果是,则直接把该对象返回;反之,会向该对象执行一次retain操作再返回。

在ARC中,通过设置和检测标志位可以移除多余的成对(“保留”&“释放”)操作,优化程序的性能。

2. __weak

2.1 weak和循环引用

__weak与我们上述所提到的__strong相对应,__strong对某个对象具有强引用。那么,__weak则指的是对某个对象具有弱引用。一般weak用来解决我们开发中遇到的循环引用问题,例如以下代码:

/*Man类*/
#import <Foundation/Foundation.h>

@class Woman;

@interface Man : NSObject

@property (nonatomic, strong)Woman *person;

@end

/*Woman类*/
#import <Foundation/Foundation.h>

@class Man;

@interface Woman : NSObject

@property (nonatomic, strong)Man *person;

@end

/*调用*/
- (void)viewDidLoad {
    [super viewDidLoad];

    Man *man = [[Man alloc] init];
    
    Woman *woman = [[Woman alloc] init];
    
    man.person = woman;
    woman.person = man;
}

从上述代码可以看出,Man类对象和Woman类对象分别有一个所有权修饰符为strong的person属性。两个类之间互相通过实例变量进行强引用,Man类对象如果要释放,则需要Woman类对象向其发送release消息。然而Woman类对象要执行dealloc方法向Man类对象发送release消息的话,又需要Man类对象向其发送release消息。双方实例变量互相强引用类对象,所以造成循环引用,如下图所示:

循环引用.png
循环引用.png

这时候weak修饰符就派上用场了,因为weak只通过弱引用来引用某个对象,并不会真正意义上的持有该对象。所以我们只需要把上述两个类对象的其中一个属性用weak来修饰,就可以解决循环引用的问题。例如我们把Woman类的person属性用weak来修饰,分析代码如下:

/*调用*/
- (void)viewDidLoad {
    [super viewDidLoad];

    Man *man = [[Man alloc] init];//Man对象引用计数为1
    
    Woman *woman = [[Woman alloc] init];//Woman对象引用计数为1
    
    man.person = woman;//强引用,Woman对象引用计数为2

    woman.person = man;//弱引用,Man对象引用计数为1
}
    //变量man超出作用域,对Man类对象的强引用失效
    //Man类对象持有者不存在,Man类对象被回收
    //Man类对象被回收, woman.person = nil;
    //Man调用dealloc方法,Woman类对象引用计数递减为1
    //变量woman超出作用域,对Woman类对象强引用失效
    //Woman类对象持有者不存在,Woman类对象被回收

从上述示例代码可以看出,我们只需要把其中一个强引用修改为弱引用就可以打破循环引用。类似的循环引用常见的有block、NSTimer、delegate等,感兴趣的自行查阅相关资料,这里不作一一介绍。

另外,基于运行时库,如果变量或属性使用weak来修饰,当其所指向的对象被回收,那么会自动为该变量或属性赋值为nil。这一操作可以有效地避免程序出现野指针操作而导致崩溃,不过强烈不建议使用一个已经被回收的对象的弱引用,这本身对于程序设计而言就是一个bug。

2.2 weak和变量

如果一个变量被__weak修饰,代表该变量对所指向的对象具有弱引用。例如以下代码:

    Person __weak *weakPerson = nil;
    
    if (1) {
        
        Person *person = [[Person alloc] init];
        weakPerson = person;
        
        NSLog(@"%@", weakPerson);//weakPerson弱引用
    }
    
    NSLog(@"%@", weakPerson);

输出结果如下:

[1198:73648] <Person: 0x600000018120>
[1198:73648] (null)

从上述输出结果可以分析,当超出作用域后,person变量对Person对象的强引用失效。Person对象持有者不存在,所以该对象被回收。同时,weakPerson变量对Person的弱引用失效,weakPerson变量被赋值为nil。

另外需要注意的是,如果一个变量被weak修饰,那么这个变量不能持有对象示例,编译器会发出警告。例如以下代码:

Person __weak *weakPerson = [[Person alloc] init];

因为weakPerson被__weak修饰,不能持有生成的Person类对象。所以Person类对象创建完立即被释放,所以编译器会给出相应的警告:

Assigning retained object to weak variable; object will be released after assignment

2.3 weak的实现
2.3.1 weak和赋值

要解释weak赋值的实现,我们先看以下示例代码:

    Person *person = [[Person alloc] init];
    
    Person __weak *p = person;

上述对应的模拟代码如下:

    Person *person = [[Person alloc] init];
    
    Person *p;
    objc_initWeak(&p, person);
    objc_destroyWeak(&p, 0);

上述两个函数分别用来对变量p的初始化和释放,它们都调用同一个函数。如下:

    id p;

    p = 0;

    objc_storeWeak(&p, person);//对应objc_initWeak

    objc_storeWeak(&p, 0);//对应objc_destroyWeak

根据上述代码我们进行分析:
1.初始化变量
当我们使用变量弱引用指向一个对象时,通过传入变量的地址和赋值对象两个参数来调用objc_storeWeak方法。该方法内部会将对象的地址&person作为键值,把变量p的地址&p注册到weak表中。

2.释放变量 当超出作用域,Person类对象被回收,此时调用objc_storeWeak(&p, 0)把变量的地址从weak表中删除。 变量地址从weak表删除前,利用被回收对象的地址作为键值进行检索,把对应的变量地址赋值为nil。

2.3.2 weak和访问

当我们访问一个被__weak修饰过的变量所指向的对象时,其内部是如何实现的?我们先看以下示例代码:

    Person *person = [[Person alloc] init];

    Person __weak *p = person;
    
    NSLog(@"%@", p);

上述代码对应的模拟代码如下:

    Person *person = [[Person alloc] init];
    
    Person *p;
    objc_initWeak(&p, person);
    id tmp = objc_loadWeakRetained(&p);
    objc_autorelease(tmp);
    NSLog(@"%@", tmp);
    objc_destroyWeak(&p);

通过上述代码可以看出,访问一个被__weak修饰的变量,相对于赋值多了两个步骤:

  1. objc_loadWeakRetained,通过该函数取出对应变量所引用的对象并retain;
  2. 把变量指向的对象注册到自动释放池。

这样一来,weak变量引用的对象就被加入到自动释放池。在自动释放池结束前都能安全使用该对象。需要注意的是,如果大量访问weak变量,就会导致weak变量所引用的对象被多次加入到自动释放池,从而导致影响性能。如果需要大量访问,我们通过__strong修饰的变量来解决,例如:

    Person __weak *p = obj;
    
    Person *tmp = p;//p引用的对象被注册到自动释放池
    
    NSLog(@"%@", tmp);
    NSLog(@"%@", tmp);
    NSLog(@"%@", tmp);
    NSLog(@"%@", tmp);
    NSLog(@"%@", tmp);

通过利用__strong中间变量的使用,p引用的对象仅注册1次到自动释放池中,有效地减少了自动释放池中的对象。

3. __unsafe_unretained

首先我们需要明确一点,如果一个变量被__unsafe_unretained修饰,那么该变量不属于编辑器的内存管理对象。该修饰符表明不保留值,即对其所指向的对象既不强引用,也不弱引用。例如以下代码:

__unsafe_unretained.png
__unsafe_unretained.png

根据上述代码分析:当超出作用域,变量p对Person类对象的强引用失效。Person类对象的持有者不存在,该对象被回收。当我们再次使用该变量的时候,因为其所表示的对象已经被回收,所以就会发生野指针崩溃。这一点区别于__weak,当被__weak修饰变量所指向的对象被回收,该变量会赋值为nil。

另外,被__unsafe_unretained修饰的变量跟__weak一样,不能持有对象实例。因为__unsafe_unretained修饰的变量不会对生成的对象实例进行保留操作,所以对象创建完即可被回收,编译器会给出相应的警告。

当我们给被__unsafe_unretained修饰的变量赋值时,必须保证赋值对象确实存在,不然程序就会发生崩溃。

4. __autoreleasing

在ARC和MRC中,autorelease作用一致,只是两者的表现方式有所不同。 1.如果返回的对象归对用者所有,如下:

    @autoreleasepool {
        
        Person __autoreleasing *p = [[Person alloc] init];
    }

上述代码模拟代码如下:

    id pool = objc_autoreleasePoolPush();
    Person *p = [[Person alloc] init];//持有
    objc_autorelease(p);
    objc_autoreleasePoolPop(pool);

2.如果返回的对象不归调用者所有,如下:

    @autoreleasepool {
        
        Person __autoreleasing *p = [Person person];
    }

上述代码模拟代码如下:

    id pool = objc_autoreleasePoolPush();
    Person *p = [Person person];
    objc_retainAutoreleasedReturnValue(p);//持有(优化后的retain方法)
    objc_autorelease(p);
    objc_autoreleasePoolPop(pool);

从上面两个例子可以看出,__autoreleasing的实现都是通过objc_autorelease()函数,只不过是持有方法有所不同而已。

至此,内存管理就介绍完毕了。由于技术有限,难免会存在纰漏,欢迎指正。转载请注明出处,万分感谢。

参考资料:
Objective-C 高级编程 iOS和OS X多线程和内存管理
Effective Objective 2.0