彻底理解 iOS 内存管理(MRC、ARC)

1. 什么是内存管理

程序在运行的过程中通常通过以下行为,来增加程序的的内存占用

创建一个OC对象
定义一个变量
调用一个函数或者方法

而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的
当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要使用的对象、变量等
如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验

所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。

那么,那些对象才需要我们进行内存管理呢?

任何继承了NSObject的对象需要进行内存管理
而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理

这是因为

继承了NSObject的对象的存储在操作系统的堆里边。
操作系统的堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
非OC对象一般放在操作系统的栈里面
操作系统的栈:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)

示例:

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        int a = 10; // 栈
        int b = 20; // 栈
        // p : 栈
        // Person对象(计数器==1) : 堆
        Person *p = [[Person alloc] init];
    }
    // 经过上面代码后, 栈里面的变量a、b、p 都会被回收
    // 但是堆里面的Person对象还会留在内存中,因为它是计数器依然是1
    return 0;
}

2. 内存管理模型

提供给Objective-C程序员的基本内存管理模型有以下3种:

自动垃圾收集(iOS运行环境不支持)
手工引用计数和自动释放池(MRC)
自动引用计数(ARC)

3.MRC 手动管理内存(Manual Reference Counting)

1. 引用计数器
系统是根据对象的引用计数器来判断什么时候需要回收一个对象所占用的内存

引用计数器是一个整数
从字面上, 可以理解为”对象被引用的次数”
也可以理解为: 它表示有多少人正在用这个对象
每个OC对象都有自己的引用计数器
任何一个对象,刚创建的时候,初始的引用计数为1

当使用alloc、new或者copy创建一个对象时,对象的引用计数器默认就是1

当没有任何人使用这个对象时,系统才会回收这个对象, 也就是说

当对象的引用计数器为0时,对象占用的内存就会被系统回收
如果对象的计数器不为0,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出 )

2. 引用计数器操作

为保证对象的存在,每当创建引用到对象需要给对象发送一条retain消息,可以使引用计数器值+1 ( retain 方法返回对象本身)
当不再需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1
给对象发送retainCount消息,可以获得当前的引用计数器值
当对象的引用计数为0时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送dealloc消息发起这个过程。
需要注意的是:release并不代表销毁\回收对象,仅仅是计数器-1

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 只要创建一个对象默认引用计数器的值就是1
        Person *p = [[Person alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 1

        // 只要给对象发送一个retain消息, 对象的引用计数器就会+1
        [p retain];

        NSLog(@"retainCount = %lu", [p retainCount]); // 2
        // 通过指针变量p,给p指向的对象发送一条release消息
        // 只要对象接收到release消息, 引用计数器就会-1
        // 只要一个对象的引用计数器为0, 系统就会释放对象

        [p release];
        // 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1
        NSLog(@"retainCount = %lu", [p retainCount]); // 1

        [p release]; // 0
        NSLog(@"--------");
    }
//    [p setAge:20];    // 此时对象已经被释放
    return 0;
}

3. dealloc方法

当一个对象的引用计数器值为0时,这个对象即将被销毁,其占用的内存被系统回收
对象即将被销毁时系统会自动给对象发送一条dealloc消息(因此,从dealloc方法有没有被调用,就可以判断出对象是否被销毁)
dealloc方法的重写

一般会重写dealloc方法,在这里释放相关资源,dealloc就是对象的遗言
一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用

- (void)dealloc
{
    NSLog(@"Person dealloc");
    // 注意:super dealloc一定要写到所有代码的最后
    // 一定要写在dealloc方法的最后面
    [super dealloc]; 
}

使用注意
不能直接调用dealloc方法
一旦对象被回收了, 它占用的内存就不再可用,坚持使用会导致程序崩溃(野指针错误)

4. 野指针和空指针

只要一个对象被释放了,我们就称这个对象为 “僵尸对象(不能再使用的对象)”
当一个指针指向一个僵尸对象(不可用内存),我们就称这个指针为野指针
只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS错误)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 执行完引用计数为1       

        [p release]; // 执行完引用计数为0,实例对象被释放
        [p release]; // 此时,p就变成了野指针,再给野指针p发送消息就会报错
        [p release];
    }
    return 0;
}

为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针
空指针
没有指向存储空间的指针(里面存的是nil, 也就是0)
给空指针发消息是没有任何反应的

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init]; // 执行完引用计数为1

        [p release]; // 执行完引用计数为0,实例对象被释放
        p = nil; // 此时,p变为了空指针
        [p release]; // 再给空指针p发送消息就不会报错了
        [p release];
    }
    return 0;
}

5. 内存管理规律
单个对象内存管理规律

谁创建谁release :

如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么你必须调用release或autorelease

谁retain谁release:

只要你调用了retain,就必须调用一次release

总结一下就是

有加就有减
曾经让对象的计数器+1,就必须在最后让对象计数器-1

多个对象内存管理规律
因为多个对象之间往往是联系的,所以管理起来比较复杂。这里用一个玩游戏例子来类比一下。
游戏可以提供给玩家(A类对象) 游戏房间(B类对象)来玩游戏。

只要一个玩家想使用房间(进入房间),就需要对这个房间的引用计数器+1
只要一个玩家不想再使用房间(离开房间),就需要对这个房间的引用计数器-1
只要还有至少一个玩家在用某个房间,那么这个房间就不会被回收,引用计数至少为1

4.ARC 自动管理内存(Automatic Reference Counting)

Automatic Reference Counting,自动引用计数,即ARC,WWDC2011和iOS5所引入的最大的变革和最激动人心的变化。ARC是新的LLVM 3.0编译器的一项特性,使用ARC,可以说一 举解决了广大iOS开发者所憎恨的手动内存管理的麻烦。
使用ARC后,系统会检测出何时需要保持对象,何时需要自动释放对象,何时需要释放对象,编译器会管理好对象的内存,会在何时的地方插入retain, release和autorelease,通过生成正确的代码去自动释放或者保持对象。我们完全不用担心编译器会出错

1. ARC的判断原则
ARC判断一个对象是否需要释放不是通过引用计数来进行判断的,而是通过强指针来进行判断的。那么什么是强指针?

强指针

默认所有对象的指针变量都是强指针
被__strong修饰的指针

 Person *p1 = [[Person alloc] init];
 __strong  Person *p2 = [[Person alloc] init];

弱指针
被__weak修饰的指针

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

ARC如何通过强指针来判断?

只要还有一个强指针变量指向对象,对象就会保持在内存中
2. ARC的使用

int main(int argc, const char * argv[]) {
    // 不用写release, main函数执行完毕后p会被自动释放
    Person *p = [[Person alloc] init];

    return 0;
}

3. ARC的注意点

不允许调用对象的 release方法
不允许调用 autorelease方法
重写父类的dealloc方法时,不能再调用 [super dealloc];

4. ARC下单对象内存管理

局部变量释放对象随之被释放

int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
    } // 执行到这一行局部变量p释放
    // 由于没有强指针指向对象, 所以对象也释放
    return 0;
}

清空指针对象随之被释放


int main(int argc, const char * argv[]) {
   @autoreleasepool {
        Person *p = [[Person alloc] init];
        p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放
    }
    return 0;
}

默认清空所有指针都是强指针

int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p1和p2都是强指针
        Person *p1 = [[Person alloc] init];
        __strong Person *p2 = [[Person alloc] init];
    }
    return 0;
}

弱指针需要明确说明
注意: 千万不要使用弱指针保存新创建的对象


int main(int argc, const char * argv[]) {
   @autoreleasepool {
        // p是弱指针, 对象会被立即释放
        __weak Person *p1 = [[Person alloc] init];
    }
    return 0;
}

5. ARC下多对象内存管理

ARC和MRC一样, 想拥有某个对象必须用强指针保存对象, 但是不需要在dealloc方法中release

@interface Person : NSObject
// MRC写法
//@property (nonatomic, retain) Dog *dog;

// ARC写法
@property (nonatomic, strong) Dog *dog;
@end

6. ARC下@property参数

strong : 用于OC对象,相当于MRC中的retain
weak : 用于OC对象,相当于MRC中的assign
assign : 用于基本数据类型,跟MRC中的assign一样

6. ARC下循环引用问题

ARC和MRC一样,如果A拥有B,B也拥有A,那么必须一方使用弱指针

@interface Person : NSObject
@property (nonatomic, strong) Dog *dog;
@end

@interface Dog : NSObject
// 错误写法, 循环引用会导致内存泄露
//@property (nonatomic, strong) Person *owner;

// 正确写法, 当如果保存对象建议使用weak
@property (nonatomic, weak) Person *owner;
@end

你可能感兴趣的:(彻底理解 iOS 内存管理(MRC、ARC))