内存管理 之 MRC、ARC(待完善)

1、概述

iOS的内存管理是指OC对象的内存管理。
在程序运行中,创建对象、定义变量、调用函数这些操作都会增加程序的内存开支。移动设备内存有限,所以需要开发者管理内存。
像int、double、char等这些基本数据类型的非OC对象类型分配在栈中,系统会自动管理。
而OC对象类型分配在堆内存中,需要开发者管理。

MRC:手动内存管理。
ARC:自动内存管理。

2、引用计数

在iOS中,使用引用计数来(retainCount)管理OC对象的内存。
系统是根据对象的引用计数器来判断什么时候回收这个对象所占用的内存

  • 当使用alloc/new/copy/mutableCopy创建一个对象时,对象的引用计数器默认就是1;在不需要这个对象时,要调用release或者autorelease来释放它;
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1;
  • 如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出);当对象的引用计数器为0时,OC对象就会销毁,释放其占用的内存空间
  • 需要注意的是:release并不代表销毁/回收对象,仅仅是计数器-1,离开自动释放池才会回收对象
3、MRC

<1>. alloc/new/copy/mutableCopy
使用alloc/new/copy/mutableCopy创建的OC对象,默认引用计数为1

<2>. retain
retain会使对象的引用计数加一。
一般通过属性来保存对象时,可以通过属性的实例变量和存取方法对这个对象进行操作。
通过retain新值,release旧值,给实例变量更新值。

- (void)setPerson:(Person *)person {
   if (_person != person) {
      [person retain];
      [_person release];
      _person = person;
}

如果我们把对象加入到数组中,那么该数组的addObject方法会对该对象调用retain(引用计数加1)。

//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
当我们持有一个对象,如果不继续使用该对象了,我们需要对其进行release释放,引用计数减一。
另外,我们也不能访问某个已经被释放的对象,该对象所占的堆空间如果被覆写就会发生崩溃的情况。

//array获得并持有NSArray类对象
NSArray *array = [[NSArray alloc] init];

/*当不再需要使用该对象时,需要释放*/
[array release];

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

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

<5>.autorelease pool《待补充》
每条线程都包含一个与其对应的自动释放池,当某条线程被终止的时候,对应该线程的自动释放池会被销毁。同时,处于该自动释放池的对象将会进行一次release操作。
当应用程序启动,系统默认会开启一条线程,该线程就是“主线程”。主线程也有一个与之对应的自动释放池,例如我们常见的ARC下的main.h文件:

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

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

//当我们执行一些复杂的操作,特别是如果这些复杂的操作要被循环执行,那么中间会免不了会产生一些临时变量。
//当被加到主线程自动释放池的对象越来越来多,却没有得到及时释放,就会导致内存溢出。
//这个时候,我们可以手动添加自动释放池来解决这个问题
for (int i = 0; i < largeNumber; i++) {
   //创建自动释放池
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   //产生许多被注册到自动释放池的临时对象
   id obj = [Person personWithComplexOperation];
   //释放池中对象
   [pool drain];     
}

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

4、ARC

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

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

可以理解,之前MRC下需要我们手动调用对象的retain和release方法。在ARC下,对象“持有”和“释放”的内存管理代码是由系统去处理的。编译器在该实例变量所属类的dealloc方法为其添加释放对象的方法。

//在ARC下,编译器会默认执行这些“持有”和“释放”的方法
//所以,我们就无需写这些代码了
- (void)dealloc {
    [p release];
    [super dealloc];
}

需要注意的是:在dealloc方法中,ARC只能帮我们处理OC对象。如果实例变量持有类似CoreFoundation等非OC对象,则需要我们手动回收:

- (void)dealloc {
    CFRelease(_cfObject);
}

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

<2>.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 

@class Woman;

@interface Man : NSObject

@property (nonatomic, strong)Woman *person;

@end

/*Woman类*/
#import 

@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消息。双方实例变量互相强引用类对象,所以造成循环引用,如下图所示:
内存管理 之 MRC、ARC(待完善)_第1张图片
6955515-25b6ea6a0921d0e4.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] 
[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修饰的变量,相对于赋值多了两个步骤:

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

这样一来,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次到自动释放池中,有效地减少了自动释放池中的对象。

可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);

6、总结
  • <1>ARC都帮我们做了什么?
    ARC是LLVM和Runtime相互协作的一个结果。
    LLVM编译器负责帮我们对代码进行编译,且加上retain、release、autoRelease这些命令。
    然后对于像weak这样的弱指针,在运行过程中,runtime监测到有对象销毁的时候,把对象对应的弱引用给销毁掉

你可能感兴趣的:(内存管理 之 MRC、ARC(待完善))