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;
- }
图片 1.PNG
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
图片 2.PNG
下面来定义两个类 玩家类: Person 和 房间类: Room
房间类: Room, 房间类中有房间号
- #import <Foundation/Foundation.h>
- @interface Room : NSObject
- @property int no; // 房间号
- @end
玩家类: Person
- #import <Foundation/Foundation.h>
- #import "Room.h"
- @interface Person : NSObject
- {
- Room *_room;
- }
- - (void)setRoom:(Room *)room;
- - (Room *)room;
- @end
现在我们通过几个玩家使用房间的不同应用场景来逐步深入理解内存管理.
1. 玩家没有使用房间, 玩家和房间之间没有联系的情况
- int main(int argc, const char * argv[]) {
- @autoreleasepool {
- // 1. 创建两个对象
- Person *p = [[Person alloc] init]; // 玩家 p
- Room *r = [[Room alloc] init]; // 房间 r
- r.no = 888; // 房间号赋值
- [r release]; // 释放房间
- [p release]; // 释放玩家
- }
- return 0;
- }
上述代码执行完前 3 行
- // 1. 创建两个对象
- Person *p = [[Person alloc] init]; // 玩家 p
- Room *r = [[Room alloc] init]; // 房间 r
- r.no = 888; // 房间号赋值
之后在内存中的表现如下图所示:
图片 3.PNG
可见, Room 实例对象和 Person 实例对象之间没有相互联系, 所以各自释放不会报错. 执行完 4,5 行代码
- [r release]; // 释放房间
- [p release]; // 释放玩家
后, 将房间对象和玩家对象各自释放掉, 在内存中的表现如下图所示:
图片 4.PNG
最后各自实例对象的内存就会被系统回收
2. 一个玩家使用一个游戏房间, 玩家和房间之间相关联的情况
- int main(int argc, const char * argv[]) {
- @autoreleasepool {
- // 1. 创建两个对象
- Person *p = [[Person alloc] init]; // 玩家 p
- Room *r = [[Room alloc] init]; // 房间 r
- r.no = 888; // 房间号赋值
- // 将房间赋值给玩家, 表示玩家在使用房间
- // 玩家需要使用这间房, 只要玩家在, 房间就一定要在
- p.room = r; // [p setRoom:r]
- [r release]; // 释放房间
- // 在这行代码之前, 玩家都没有被释放, 但是因为玩家还在, 那么房间就不能销毁
- NSLog(@"-----");
- [p release]; // 释放玩家
- }
- return 0;
- }
上边代码执行完前 3 行的时候和之前在内存中的表现一样, 如图
图片 3.PNG
当执行完第 4 行代码 p.room = r; 时, 因为调用了 setter 方法, 将 Room 实例对象赋值给了 Person 的成员变量, 不做其他设置的话, 在内存中的表现如下图(做法不对):
图片 5.PNG
在调用 setter 方法的时候, 因为 Room 实例对象多了一个 Person 对象引用, 所以应将 Room 实例对象的引用计数 + 1 才对, 即 setter 方法应该像下边一样, 对 room 进行一次 retain 操作.
- - (void)setRoom:(Room *)room // room = r
- {
- // 对房间的引用计数器 + 1
- [room retain];
- _room = room;
- }
那么执行完第 4 行代码 p.room = r;, 在内存中的表现为:
图片 6.PNG
继续执行第 5 行代码[r release];, 释放房间, Room 实例对象引用计数 - 1, 在内存中的表现如下图所示:
图片 5.PNG
然后执行第 6 行代码[p release];, 释放玩家. 这时候因为玩家不在房间里了, 房间也没有用了, 所以在释放玩家的时候, 要把房间也释放掉, 也就是在 delloc 里边对房间再进行一次 release 操作.
这样对房间对象来说, 每一次 retain/alloc 操作都对应一次 release 操作.
- - (void)dealloc
- {
- // 人释放了, 那么房间也需要释放
- [_room release];
- NSLog(@"%s", __func__);
- [super dealloc];
- }
那么在内存中的表现最终如下图所示:
图片 7.PNG
最后实例对象的内存就会被系统回收
3. 一个玩家使用一个游戏房间 r 后, 换到另一个游戏房间 r2, 玩家和房间相关联的情况
- int main(int argc, const char * argv[]) {
- @autoreleasepool {
- // 1. 创建两个对象
- Person *p = [[Person alloc] init]; // 玩家 p
- Room *r = [[Room alloc] init]; // 房间 r
- r.no = 888; // 房间号赋值
- // 2. 将房间赋值给玩家, 表示玩家在使用房间
- p.room = r; // [p setRoom:r]
- [r release]; // 释放房间 r
- // 3. 换房
- Room *r2 = [[Room alloc] init];
- r2.no = 444;
- p.room = r2;
- [r2 release]; // 释放房间 r2
- [p release]; // 释放玩家 p
- }
- return 0;
- }
执行下边几行代码
- // 1. 创建两个对象
- Person *p = [[Person alloc] init]; // 玩家 p
- Room *r = [[Room alloc] init]; // 房间 r
- r.no = 888; // 房间号赋值
- // 2. 将房间赋值给玩家, 表示玩家在使用房间
- p.room = r; // [p setRoom:r]
- [r release]; // 释放房间 r
之后的内存表现为:
图片 8.PNG
接着执行换房操作而不进行其他操作的话,
- // 3. 换房
- Room *r2 = [[Room alloc] init];
- r2.no = 444;
- p.room = r2;
内存的表现为:
图片 9.PNG
最后执行完
- [r2 release]; // 释放房间 r2
- [p release]; // 释放玩家 p
内存的表现为:
图片 10.PNG
可以看出房间 r 并没有被释放, 这是因为在进行换房的时候, 并没有对房间 r 进行释放. 所以应在调用 setter 方法的时候, 对之前的变量进行一次 release 操作. 具体 setter 方法代码如下:
- - (void)setRoom:(Room *)room // room = r
- {
- // 将以前的房间释放掉 -1
- [_room release];
- // 对房间的引用计数器 + 1
- [room retain];
- _room = room;
- }
- }
这样在执行完 p.room = r2; 之后就会将 房间 r 释放掉, 最终内存表现为:
图片 11.PNG
4. 一个玩家使用一个游戏房间, 不再使用游戏房间, 将游戏房间释放掉之后, 再次使用该游戏房间的情况
- int main(int argc, const char * argv[]) {
- @autoreleasepool {
- // 1. 创建两个对象
- Person *p = [[Person alloc] init];
- Room *r = [[Room alloc] init];
- r.no = 888;
- // 2. 将房间赋值给人
- p.room = r; // [p setRoom:r]
- [r release]; // 释放房间 r
- // 3. 再次使用房间 r
- p.room = r;
- [r release]; // 释放房间 r
- [p release]; // 释放玩家 p
- }
- return 0;
- }
执行下面代码
- // 1. 创建两个对象
- Person *p = [[Person alloc] init];
- Room *r = [[Room alloc] init];
- r.no = 888;
- // 2. 将房间赋值给人
- p.room = r; // [p setRoom:r]
- [r release]; // 释放房间 r
之后的内存表现为:
图片 12.PNG
然后再执行 p.room = r;, 因为 setter 方法会将之前的 Room 实例对象先 release 掉, 此时内存表现为:
图片 13.PNG
此时_room,r 已经变成了一个野指针. 之后再对野指针 r 发出 retain 消息, 程序就会崩溃. 所以我们在进行 setter 方法的时候, 要先判断一下是否是重复赋值, 如果是同一个实例对象, 就不需要重复进行 release 和 retain. 换句话说, 如果我们使用的还是之前的房间, 那换房的时候就不需要对这个房间再进行 release 和 retain. 则 setter 方法具体代码如下:
- - (void)setRoom:(Room *)room // room = r
- {
- // 只有房间不同才需用 release 和 retain
- if (_room != room) { // 0ffe1 != 0ffe1
- // 将以前的房间释放掉 -1
- [_room release];
- // 对房间的引用计数器 + 1
- [room retain];
- _room = room;
- }
- }
因为 retain 不仅仅会对引用计数器 + 1, 而且还会返回当前对象, 所以上述代码可最终简化成:
- - (void)setRoom:(Room *)room // room = r
- {
- // 只有房间不同才需用 release 和 retain
- if (_room != room) { // 0ffe1 != 0ffe1
- // 将以前的房间释放掉 -1
- [_room release];
- _room = [room retain];
- }
- }
以上就是 setter 方法的最终形式.
来源: https://juejin.im/post/5c98ad516fb9a070d90f6c3a