【Objective-C】6 内存管理
第六节 内存管理
01 内存管理概述
内存的作用 : 存储数据
三个问题:
-
如何将数据存储到内存中?
声明一个变量(申请一块指定字节数的地址空间),将数据赋值给变量
-
当数据不再被使用时,如何释放其占用的内存空间?
栈:存储局部变量。当局部变量的作用域被执行完毕时,就会立刻被系统回收 ---> 找大括号
BSS 段:未初始化的全局变量 & 静态变量。一旦初始化就会回收 BSS 段中的空间,并转存到数据段中
数据段(常量区):已经初始化的全局变量 & 静态变量。直到程序结束才会回收。
代码段:代码。程序结束时系统会自动回收。
---> 在内存中,以上四个区域中的数据回收由系统自动完成,无需系统外界干预
-
内存中的堆区如何释放?
堆:存储 OC 对象。在程序运行结束前,堆区中存储的数据不会被系统主动释放。
-
iPhone 内存机制:40M - 警告 / 45M - 警告 / 120M - 强制结束程序(闪退)
-
问题:如果程序长时间运行(比如游戏),且始终不清理堆区数据,就会占用过多内存空间,进而闪退
---> 内存管理
-
内存管理的范围:只管理存储在堆中的OC对象的回收(其他区域由系统自动完成,无需管理)
何时回收?:当不再有指针指向这个对象的时候,即 该对象不再被使用的时候
---> 引用计数器
1.1 引用计数器
每个对象都有一个属性:retainCount,称为 引用计数器,类型为 unsigned long,占 8 个字节
作用:记录此时该对象有多少个引用(此时有多少指针指向这个对象 / 有多少地方正在使用该对象)
Person *p = [Person new]; // 此时 p 指向的对象中的 retainCount 的值为 1
当对象的 retainCount = 0 时,表示当前这个对象不被使用,此时系统会自动回收这个对象
操作流程:
- 当对象增加一处引用时,会向对象发送一条 retain 消息,对象的 retainCount 会 + 1
- 当对象减少一处引用时,会向对象发送一条 release 消息, retainCount -= 1
- 向对象发送一条 retaincount 消息,可以取到 retainCount 的值
- 当 retainCount == 0 时,对象会被系统立即回收,此时会自动调用对象的 dealloc 方法
1.2 内存管理的分类
内存管理有两种方式:
-
MRC:Manual Reference Counting,手动引用计数 / 手动内存管理
当引用数量变化时,要求程序员手动发送 retain 消息 / release消息
-
ARC:Automatic Reference Counting,自动... / ...
系统自动在合适的位置发送 retain / release 消息
学习 MRC 目的:
1)面试。。。
2)iOS 5 (Xcode 4.2,2011年)以前,只有 MRC 方式 ---> 早期 app
3)MRC 更精确些
4)ARC 是基于 MRC 的
02 第一个 MRC 程序
-
当前 Xcode 默认支持 ARC 开发。如需使用 MRC ,首先需要关闭 ARC 模式(ARC 不支持手写 retain / release):
选择当前 targets,在 build settings - all - 【apple LLVM... objective C】下找到 OC ARC,并关闭
(可以直接在 all 界面中搜索 auto)
-
当系统回收对象时,会自动调用对象的 dealloc 方法 ---> 重写方法
重写 dealloc 方法的规范:必须在最后一句代码调用父类的dealloc 方法 (早来晚走),否则系统会自动警告
@implementation Student - (void)dealloc{ // 重写 dealloc 方法 NSLog(@"释放当前 Student类 的对象"); [super dealloc]; // 在方法最后,调用父类的 dealloc 方法 } // 当前对象已被回收 @end -
测试引用计数器
手动减小 retainCount 的引用计数值:
- 新创建一个对象,此时当前对象的引用计数器默认为 retainCount = 1
- 使用当前对象调用 release 方法,使 retainCount--
- 此时 retainCount == 0,系统立即执行该对象的 dealloc 方法,并回收该对象
手动增加 retainCount 的引用计数值:
- 新创建一个对象
- 使用当前对象调用 retain 方法,使 retainCount++
- 使用点语法取出 retainCount 的值,检查值的大小是否增加
Person *p1 = [[Person alloc] init]; [p1 release]; // 该语句相当于手动使 p1.retainCount 减1,执行后 reatinCount = 0 // 此时,系统自动执行 [p1 dealloc]; Person *p2 = [[Person alloc] init]; [p2 retain]; // 该语句相当于手动使 p2.retainCount 加1 NSLog(@"count = %d", p2.retainCount); // 输出 count = 2 [p1 release]; // reatinCount--, 执行后 retainCount = 1 // retainCount 值不为 0 ,系统不会回收对象的数据 注意:
-
调用 release 方法不等于让内存释放空间,只是使对象的 retainCount 的值减 1
-
在 ARC 机制下, retain / release / dealloc 这些方法无法使用
03 内存管理原则
- 有对象的创建,就要匹配一个 release
- retian 的次数应与 release 的次数匹配
- 谁用谁 retain ,谁不用谁 release (看准操作的对象)
- 只有在多一处引用的时候才 retain,少一处引用的时候才 release
---> retain & release 要平衡
04 野指针与僵尸对象
-
C 语言中的野指针:在定义一个指针变量时,如果没有为其初始化,那么该指针存储的是一个垃圾值,指针指向一块随机的地址空间。
OC 中的野指针:指针指向的对象已被系统回收
-
内存回收的本质:变量被回收时,表示该变量原本占用的地址空间现在可以被重新分配出去。但在该地址空间被重新占用之前,其中存储的数据不会发生改变
对象回收的本质:对象占用的空间可以被重新分配,但在分配出去之前,其中存储的对象数据还在。
-
僵尸对象:一个已经被释放的对象,但这个对象所占用的空间可能还没有重新分配出去
---> 使用野指针来访问僵尸对象时,有可能有问题(已重新分配),也有可能没问题(还没重新分配)
不建议访问僵尸对象 ---> 使用 Xcode 自动检查僵尸对象的机制(不设置则系统允许访问):
- 选择左上 停止符号 右侧的框 - edit scheme
- 选择 run - diagnostics,选中 enable zombie objects,关闭菜单栏
-
为何不默认打开僵尸对象检测?
一旦打开,在每访问一个对象时,系统都会先自动检查这个对象是否为僵尸对象 ---> 非常消耗性能
-
如何避免僵尸对象错误?
当一个指针成为野指针后,将指针的值设为 nil
此时,如果使用该指针调用对象方法(包括使用点语法时),系统不会报错,也不会有任何执行结果(该语句可通过编译但无效);但如果通过该指针直接访问属性(使用 ->),系统会报错
-
无法“复活”僵尸对象
Person *p = [[Person alloc] init]; // ... // 假设此时 p.retainCount = 1 [p release]; // p.retainCount = 0,立即释放 p 指向的对象,p 指针成为野指针 // [p retain]; // 此时,该语句无效,因为 p 指向的对象已被释放
05 单个对象的内存管理
5.1 内存泄露
一个对象没有被及时回收(在该回收的时候没有被系统回收,而是继续留在内存中,直到程序结束时被回收)
5.2 单个对象的内存泄露
几种可能发生单个对象的内存泄露的情况:
-
有对象的创建,但没有对应的 release
-
retain & release 的次数不匹配
-
在不恰当的时候,为指向当前对象的指针赋值为 nil
-
在方法中为传入的对象进行不适当的retian
Person *p = [[Person alloc] init]; // p.retainCount = 1 God *g = [[God alloc] init]; // g.retainCount = 1 // 假设 God 类中有 对象方法 - (void)killWithPerson:(Person *)p // 该方法中存在 retain 语句,使传入对象的 引用计数 + 1 (地址传递,方法内可以改变传入的参数指针的指向的数据值) [g killWithPerson:p]; // 语句执行结束后,p.retainCount = 2 [g release]; // g.retainCount = 0,系统回收 g 指向的对象,g 成为野指针 [p release]; // p.retainCount = 1, 不会被回收
- 如何保证单个对象可以被回收?
- 每创建一个新的对象,就必须为其匹配一个 release
- retain & release 的次数必须匹配
- 只有指针成为野指针时才将其赋值为 nil
- 在方法中要严谨使用 retain
06 多个对象的内存管理
6.1 举例分析
-
例:
Perosn *p = [Perosn new]; p.name = @"jack"; Car *c = [Car new]; c.speed = 10; p.car = c; [p driveOwnCarToCity:@"Beijing"]; [c release]; [p driveOwnCarToCity:@"Shanghai"]; // 该语句可通过编译,但若设置了僵尸对象检测,那么执行时会报错 // 问题出在 p.car = c; // 该语句使得对象多了一条引用,但引用计数器并没有 + 1 // 解决方法:重写 - (void)setCar:(Car *)car 方法 解决方法:重写 setter 方法,在方法内部调用 retian 方法(因为该属性是一个指向类的对象的指针,调用该属性的 setter 方法相当于创建了一个指向指定对象的新指针,即给该对象增加了一条新引用)
- (void)setCar:(Car *)car{ _car = [car retian]; // 给传进来的对象发送一条 retain 消息 // retain 方法的返回值为 instancetype,所以可直接放在等号右侧,赋值给当前属性 } 修改 setter 方法后:
Perosn *p = [Perosn new]; // p 1 Car *c = [Car new]; // c 1 p.car = c; // c 2 [p driveOwnCarToCity:@"Beijing"]; [c release]; // c 1 [p driveOwnCarToCity:@"Shanghai"]; [p release]; // p 0, p 指向的对象被回收,p 成为野指针 // 新的问题:c 应该何时回收?如何回收? // // 何时? ---> p 被回收时,c 就应该被回收(因为本例情景中,车只通过人来使用,人消失则车也应该消失) // 如何随 p 一起回收? ---> 改写 dealloc 方法,在其中添加一句 [_car release]; // [_car release]; 表示在当前对象被回收时,_car 指向的对象的引用应减 1 // 具体 _car 指向的 Car 类的对象是否需要回收,由 Car 对象的 retainCount 决定 --> 更严谨 -
小结 1:当类的属性是一个 OC 对象时,需要注意重写 setter & dealloc的写法
- (void)setCar:(Car *)car{ _car = [car retain]; // 表示多了一个指针指向传入方法的参数 car 对象,即该对象多了一处引用}- (void)dealloc{ [_car release]; // 与 setter 方法中的 retain 呼应 [super dealloc];} ---> 但其实还存在问题(bug):如果中间给 _car 重新赋值...?
// Person 类中的 setter & dealloc方法已重写 Perosn *p = [Perosn new]; // p 1 Car *c1 = [Car new]; // c1 1 p.car = c1; // c1 2 // 更改 p 对象中的 car 指针 属性,使其指向新的对象 c2Car *c2 = [Car new]; // c2 1 p.car = c2; // c2 2 [c1 release]; // c1 1 [c2 release]; // c2 1 [p release]; // p 0 --> p 对象被回收,并自动调用 dealloc 方法 // dealloc 方法中包含 [_car release]; 语句 --> c2 0 ---> 回收 c2 对象 // 问题 - c1 对象发生内存泄露:如何回收 c1? 原因分析:在引用改变时,没有及时调整原先指向的对象中的 retainCount(减 1)
解决方法:在使用 setter 方法赋值时,应首先让 _car 属性原本指向的对象 release,然后再使用 [car retain] 将传入的参数赋值给 _car ,并给传进来的对象发送一条 retain 消息,使其 retainCount 值 + 1
- (void)setCar:(Car *)car{ [_car release]; // 解除属性对当前对象的引用,为属性当前指向的对象的引用计数器 - 1 _car = [car retian];
// 给传进来的对象发送一条 retain 消息,返回 instancetype}
修改 setter 方法后: ```objective-c Person *p = [Person new]; // p 1 Car *c1 = [Car new]; // c1 1 p.car = c1; // c1 2 Car *c2 = [Car new]; // c2 1 p.car = c2; // 先给原指向的对象减 1:c1 1 // 再给新的对象加 1: c2 2 [c1 release]; // c1 0,回收 [c2 release]; // c2 1 [p release]; // p 0,回收 p --> c2 0,回收 c2
---> 目前仍存在问题(bug):如果将 c1 重复赋值给 _car ...?
Person *p = [Person new]; // p 1 Car *c1 = [Car new]; // c1 1 p.car = c1; // c1 2 [p drive]; [c1 release]; // c1 使用完毕,释放一处引用 --> c1 1 c1.speed = 100; // 此时想给 c1 重新赋值,然后再重新使用p.car = c1; // 根据重写的 setter 方法,执行流程为: // 1. 先给原指向的对象减 1:c1 0 // 2. 再给新的对象加 1 // 在执行第一步时,由于 c1 对象中的引用计数器值为 0,系统立即回收 c1 对象 // 执行到第二步时,c1 对象已经是一个僵尸对象了,无法“复活”
原因分析:新赋值给指针的对象其实就是指针当前指向的对象
解决方法:在 setter 方法中添加判断语句,若新旧对象不相同才执行方法
- (void)setCar:(Car *)car{ if(_car != car){ // 先判断,符合条件再执行 [_car release]; _car = [car retian]; } }
6.2 总结:如何处理类的属性是一个对象的情况
-
重写 setter 方法
- (void)setCar:(Car *)car{ if(_car != car){ [_car release]; _car = [car retian]; // 在给这种属性赋值时(不包括初始化),有一个 release 就要对应有一个 retain } } -
重写 dealloc 方法
- (void)dealloc{ [_car release]; // 与 setter 方法中的 retain 呼应 [super dealloc]; }
注意:
- 注意内存管理范围:只有当类的属性为一个 OC 对象时,才需要这样特别处理 setter 和 dealloc
- 注意 NSString *,也是一个 OC 对象,也需要做上述处理
07 @property 参数(1)
7.1 @property 概述
【回顾】@property 作用:
-
自动生成私有属性
-
自动生成 setter & getter 方法的声明
-
自动生成 setter & getter 方法的实现
--> setter 方法均是直接使用传入的参数赋值
问题:在 MRC 模式下,若想满足前面对 setter 方法的改动,应该如何使用 @property ?(不重写所有setter)
---> 令 @property 携带参数
语法格式:【 @property( 参数 1,参数 2 ,... ) 数据类型 名称 】 // 参数个数不限
@property 的四组参数:
- 与多线程相关的 2 个参数:atomic、nonatomic
- 与生成的 setter 方法的实现相关的 2 个参数:assign、retain
- 与生成 只读 / 读写 相关的 2 个参数:readonly、readwrite
- 与生成的 setter & getter 方法的名字相关的 2 个参数:getter、setter
注意:前三组参数每组最多只能写一个
7.2 多线程相关:atomic & nonatomic
-
atomic:默认值。此时生成的 setter 方法的代码会被加上一把线程安全锁(避免多个线程对属性同时访问)
---> 特点:安全,但效率低
-
nonatomic:不会添加线程安全锁。---> 不安全,但效率高
建议:推荐选择效率,使用 nonatomic(在没有讲解多线程知识前,一律使用 nonatomic)
7.3 setter 方法实现相关:assign & retain
-
assign:默认值。生成的 setter 方法的实现就是直接将参数赋值给属性
-
retain:生成的 setter 方法的实现是标准的 MRC 内存管理代码
即 1)先判断, 2)release 旧对象,3)retain 新对象
建议:当属性是 OC 对象时,就使用 retain;否则使用 assign
注意:使用 retain 参数只会自动生成对应的 setter 方法,不会改变 dealloc。dealloc 方法仍需要程序员手动重写。
7.4 只读 & 读写:readonly & readwrite
- readwrite:默认值。表示会同时自动生成 setter & getter
- readonly:只会生成 getter 方法 ---> 只能取值,无法赋值
7.5 setter & getter 方法的名字相关:setter & getter
默认情况下, 由 @property 自动生成的 setter & getter 方法的名字就是 setAge: / age
但可以通过参数 setter = xxxxxx: / getter = xxxxx 来自定义方法的名字
注意:
-
setter 方法有参数,需要加 :
-
不影响点语法的使用。
点语法内部实现原理:编译器编译时,会将点语法转化为调用 setter / getter 的代码
如果使用参数 setter / getter 修改了生成的方法的名字,编译器会将点语法转换为调用修改后的名字的代码
建议:不建议自定义方法名字(不符合标准)
-
尤其是 setter 方法的名字,不要修改
-
当属性为 BOOL 类型时,可以修改 getter 的名字,使用 is 开头来增强可读性
@property(nonatomic, assign, getter = isGoodMan) BOOL goodMan;p.isGoodMan = YES;
注意:@property 不只上述提到的这些参数,还有 strong & weak (用于ARC模式下,见第七节)、copy(见第十节)等
08 @class
问题:每个人有一本不同的书 --> Book 类的对象是 Person 类的属性
此时书和人一一对应,如何找到每本书的拥有者?
--> 在 Book 类中添加一个 Person 对象作为【拥有者】属性
新的问题:循环引用
当两个类相互包含时(Person.h 中含有 Book.h,Book.h 中也包含 Person.h),会出现循环引用的问题,造成无限递归,无法通过编译。
---> 取消一个头文件引入,换成使用 @class 来告诉编译器引入一个类(例:在 Person. h 中使用 @class Book; )
-
@class 与 #import 区别
import 会将头文件中的内容拷贝到当前位置
@class 并不会拷贝任何内容,只是告诉编译器这是一个类,使通过编译
-
使用建议:在 .h 文件中使用 @class,在 .m 文件中仍使用 #import(这样在编辑时可以提醒头文件中的属性名 / 方法名,方便使用)
09 循环 retain
问题:当 Person 对象 与 Book 对象互为对方的属性时,retainCount 的情况?
Person *p = [Person new]; // p 1 Book *b = [Book new]; // b 1 p.book = b; // b 2 b.owner = p; // p 2 // 使用结束,准备释放 p b 2 个对象 [p release]; // 减一: p 1 [b release]; // 减一: b 1 // 因为两个对象相互引用,此时两个对象均无法释放 // ---> 内存泄露(对象无法及时释放)
原因分析:两个对象相互引用时,执行了两次赋值操作,对两个对象都使用了 retain,导致发生了内存泄露
解决方法( 2 个步骤 ):
-
在其中一个类中,将对应另一个类的对象的属性声明改为 @property(..., assign, ...) Book *book;
也就是在点语法调用 setter 方法时,只对一个对象使用 MRC 内存管理代码 -
修改对应类中的 dealloc 方法,删去其中的 [_book release]; 语句
因为在声明属性时就没有把它当作 OC 对象处理,没有使用 retian --> retain & release 个数要匹配
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!