OC 中的内存管理笔记

注: 本文”人”表示对象的使用者

一、概述

1. 内存中有五大区域, 为什么只需要关心堆区?

  • 堆区存的是对象, 对象占的区域可能会很大
  • 堆区的对象不会自动释放, 需要手动管理, 如果不进行管理, 那么会直到程序结束的时候才会释放

2. 什么时候释放堆区的对象?

  • 当这个OC对象没有”人”使用的时候, 可以释放
  • 当这个OC对象有”人”使用的时候, 千万不要释放

3. 如何判断一个OC对象是否有人使用?

  • 每一个OC对象都有一个引用计数器(retainCount), 表示对象的使用者的个数
  • 每当创建一个新的对象, retainCount值默认是1
  • 每当多一个人要使用对象的时候, retainCount+1
  • 每当少一个人要使用对象的时候, retainCount-1
  • 当retainCount=0的时候, 系统会立即回收该对象, 同时给对象发送dealloc消息

4. 程序员如何控制retainCount值?

  • 向对象发送release消息, 让对象的retainCount-1
  • 向对象发送retain消息, 让对象的retainCount+1
  • 向队形发送retainCount消息, 获得对象的retainCount值
  • 所谓的消息, 其实就是对象的方法

所有和内存管理相关的成员变量以及方法, 都是定义在 NSObject 类中的

5. 内存管理的分类

  • MRC, 手动引用计数, 程序员需要手动管理对象的retainCount值
  • ARC, 自动引用计数, 编译器帮我们管理对象的retainCount值
  • ARC 和 MRC 的关系
    1. ARC是基于MRC的
    2. ARC 是编译器特性, 编译器在编译的时候, 会在合适的位置, 自动插入内存管理的代码
    3. ARC 是 2011年 iOS 5 出现, 是程序员的一大福音, 省去了很多繁琐的内存管理的工作
    4. 从 Xcode 6 开始, 新建的项目默认都是 ARC 的

6. MRC 是 ARC 的底层原理

只有理解 MRC 才能更好的理解内存管理的原理

二、MRC 程序

1. 如何关闭ARC机制, 把项目设置为 MRC?

在项目信息页面, 选择BuildSettings, 搜索auto, 找到”A…R…C…”,默认是 YES, 改成 NO 即可

注意: 如果在 ARC 环境下, 不能使用手动内存管理的方法, 否则编译器会报错

2. 为什么我们重写 dealloc 方法的时候, 需要在最后调用父类的 dealloc 方法?

重写init方法的时候

  • 父类的init方法实现默认有一些初始化对象时必须的操作
  • 需要在父类init方法的基础之上增加一些操作, 这个时候需要先调用父类中init方法实现

重写dealloc方法的时候

  • 父类的dealloc方法实现中默认有一些对象释放的是否必须的操作, 一旦调用了NSObject类中dealloc方法的实现, 那么对象就会被彻底的释放
  • 我们在对象彻底释放之前可能还需要对对象做一些事情, 比如说需要先释放当前对象中特有一些内容, 在完成这些操作之后, 再调用父类中dealloc方法的实现

3. 注意点:

  • 重写dealloc方法的时候, 必须在最后调用父类中dealloc方法的实现(MRC)
  • dealloc方法不需要手动调用, 当对象销毁的时候系统会自动调用该对象的dealloc方法, 不能手动调用
  • 对象调用release方法之后不一定会销毁, release方法只会让当前对象的引用计数器-1, 和销毁对象之间没有什么必然联系

在MRC中判断对象是否销毁只有一个原则, 就是对象的引用计数器是否为0

三、内存管理的原则

1. 保持平衡

  • 有new/alloc, 就必须要有release
  • 有retain, 就必须要有release
  • 谁new/alloc, 谁release
  • 谁retain, 谁release

2. 使用哪个指针给对象发送了new/alloc消息, 就需要通过这个指针发送release

3.使用哪个指针个对象发送了retain消息, 就需要通过这个指针发送release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Person *p1 = [[Person alloc] init];  //1
//[p1 release];

//Person *p2 = p1;
//[p2 retain];
// retain内部把当前对象的retainCount+1, 然后返回当前对象

Person *p2 = [p1 retain];  // 2

[p1 release] // 1
[p2 release] // 0 -> 由系统调用dealloc方法, 释放对象

//
//Person *p2 = [p1 retain];
//Person *p3 = p1;
//
//[p1 release];
//[p2 release];

四、野指针与僵尸对象

在OC中

1. 对象的回收

  • 一旦对象回收, 说明对象在内存中占有的堆区空间, 可以分配给别人使用(不代表空间被删掉, 也不代表立刻分配给被人使用)
  • 但是在系统把该空间分配给其他人使用之前, 对象还存在(对象中的数据还存在)

2. 僵尸对象

对象已经被回收了, 但是对象的数据还存在内存中, 这样的对象叫做僵尸对象
注意点:

  • 当僵尸对象占用的空间, 系统没有分配给其他人使用的时候, 这个对象其实还存在, 还可以访问
  • 当僵尸对象占用的空间, 系统已经分配给其他人使用的时候, 这个对象不存在, 不可以访问
  • 严格意义上来说, 对象一旦成为了僵尸对象, 无论如何不能访问
  • 僵尸对象无法复活(不应该调用僵尸对象的方法)

Xcode中开启僵尸对象检测

  • 默认没有开启, 避免性能浪费

开启方法

Edit Scheme -> Diagnostics -> Enable Zombie Object

3. 野指针

  • 指向被回收对象的指针, 叫做野指针
  • 指向堆区僵尸对象的栈区指针变量, 叫做野指针

4. 如何防止野指针错误

1
2
3
4
5
6
Person *p1 = [[Person alloc] init]; // 1
[p1 release] // 0
// p1是一个野指针, p1指向的堆区对象是僵尸对象
p1 = nil;

[nil sayHi];

五、单个对象的内存管理

1
2
3
int main() {
    Person *p1 = [Person new];
}

如何避免内存泄漏

  1. retain和release一定要匹配
  2. 不要让指针随便指向nil

六、多个对象的内存管理之一

当对象A中拥有一个成员变量是对象B, 如果A没有释放, 那么B就不能是释放. 因为B作为A的成员变量, 说明A随时都有可能使用B, 那么必须当A释放的时候, 才能把B释放

演化步骤:

  1. 创建对象A, 创建对象B
  2. 把B对象设置给A对象

出现问题:
当B对象释放之后, A对象就无法再使用B对象

解决方案:

在把B对象设置给A的时候, 让B对象的引用计数器+1, 说明A对象拥有B对象

出现问题:
当A对象释放之后, B对象无法释放

解决方案:

当A对象销毁的时候,也就是在A对象的dealloc方法中, 先让B对象的引用计数器-1, 再释放A对象

七、多个对象的内存管理之二

出现问题:
如果给A类对象a中的B类型的成员变量更换了一个值, 从b对象换成了b1, 此时在a对象销毁的时候, 释放的是b1, 而无法释放b, 导致b内存泄漏

解决方案:

在B类型成员变量的set方法中, 先release旧对象b, 再retain新对象b1

八、回收分析

两个A类型的不同对象a和a1, 同时拥有一个B类型的成员和变量b, 没有出现任何问题

九、多个对象的内存管理之三

出现问题:
如果给A类对象a中的B类型成员变量重复赋同一个值, 之前是b, 再赋一次b因为在B类型的成员变量set方法中, 是先release再retain,此时有可能导致b对象在release之后已经释放, 变成僵尸对象, 再进行retain操作会造成野指针错误

解决方案:

在B类型成员变量的set方法中, 需要进行判断, 如果是相同对象什么都不做, 如果不是相同对象, 先release旧对象, 再retain新对象

1
2
3
4
5
6
7
8
9
10
11
- (void)setB:(B *)b {
    if (_b != b) {
        [_b release];
        _b = [b retain];
    }
}

- (void)dealloc {
    [_b release];
    [super dealloc];
}

十、当属性的类型是OC对象的时候, setter方法的写法

注意:

当成员变量是OC对象的时候, 它的set方法才需要按照以上总结的内容来写
当成员变量是基本数据类型的时候, 直接赋值即可

十一、@property参数之与多线程相关

多线程相关

  • atomic(默认值) 线程安全
  • nonatomic 线程不安全

十二、@property参数之与生成setter方法实现相关

在MRC下和内存管理相关的参数

assign(默认)
retain
如果成员变量是OC对象, 使用retain
如果成员变量是非OC对象, 使用assign

十三、@property其他参数

  1. 读写相关
  • readwrite(默认), 可读可写, 会同时生产getter和setter
  • readonly, 只读, 只会生产getter, 而不会生成setter
  1. 指定setter和getter名字
    注意:
  • 如果需要设置setter的名字, 一定要加:. 但是无论如何都不要更改setter的名字
  • 如果成员变量是BOOL类型, getter一般以is开头, 增加可读性
  • 只是改变了setter和getter的名字, 并没有改变内部实现

十四、@property参数使用注意

  1. 在使用@property的时候,必须要加参数
  • 每种类型的参数, 同时只能存在一个
  • 至少需要两类: 线程相关的, 内存管理相关的
    规范写法:@property (nonatomic, retain) NSString *name;
  1. 如果在MRC环境下
  • 当使用@property定义属性的时候, 如果使用reatin, 只会在setter中生成标准的MRC内存管理代码
  • 但是dealloc中对该成员变量, 并没有release, 需要我们自己来做
0%