【Objective-C】6 内存管理

第六节 内存管理


01 内存管理概述

内存的作用 : 存储数据

三个问题:

  1. 如何将数据存储到内存中?

    声明一个变量(申请一块指定字节数的地址空间),将数据赋值给变量

  2. 当数据不再被使用时,如何释放其占用的内存空间?

    栈:存储局部变量。当局部变量的作用域被执行完毕时,就会立刻被系统回收 ---> 找大括号

    BSS 段:未初始化的全局变量 & 静态变量。一旦初始化就会回收 BSS 段中的空间,并转存到数据段中

    数据段(常量区):已经初始化的全局变量 & 静态变量。直到程序结束才会回收。

    代码段:代码。程序结束时系统会自动回收。

    ---> 在内存中,以上四个区域中的数据回收由系统自动完成,无需系统外界干预

  3. 内存中的堆区如何释放?

    堆:存储 OC 对象。在程序运行结束前,堆区中存储的数据不会被系统主动释放。

    • iPhone 内存机制:40M - 警告 / 45M - 警告 / 120M - 强制结束程序(闪退)

    • 问题:如果程序长时间运行(比如游戏),且始终不清理堆区数据,就会占用过多内存空间,进而闪退

      ---> 内存管理

内存管理的范围:只管理存储在堆中的OC对象的回收(其他区域由系统自动完成,无需管理)

何时回收?:当不再有指针指向这个对象的时候,即 该对象不再被使用的时候
---> 引用计数器

1.1 引用计数器

每个对象都有一个属性:retainCount,称为 引用计数器,类型为 unsigned long,占 8 个字节

作用:记录此时该对象有多少个引用(此时有多少指针指向这个对象 / 有多少地方正在使用该对象)

Person *p = [Person new]; // 此时 p 指向的对象中的 retainCount 的值为 1

当对象的 retainCount = 0 时,表示当前这个对象不被使用,此时系统会自动回收这个对象

操作流程:

  1. 当对象增加一处引用时,会向对象发送一条 retain 消息,对象的 retainCount 会 + 1
  2. 当对象减少一处引用时,会向对象发送一条 release 消息, retainCount -= 1
  3. 向对象发送一条 retaincount 消息,可以取到 retainCount 的值
  4. 当 retainCount == 0 时,对象会被系统立即回收,此时会自动调用对象的 dealloc 方法

1.2 内存管理的分类

内存管理有两种方式:

  1. MRC:Manual Reference Counting,手动引用计数 / 手动内存管理

    当引用数量变化时,要求程序员手动发送 retain 消息 / release消息

  2. 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 的引用计数值:

    1. 新创建一个对象,此时当前对象的引用计数器默认为 retainCount = 1
    2. 使用当前对象调用 release 方法,使 retainCount--
    3. 此时 retainCount == 0,系统立即执行该对象的 dealloc 方法,并回收该对象

    手动增加 retainCount 的引用计数值:

    1. 新创建一个对象
    2. 使用当前对象调用 retain 方法,使 retainCount++
    3. 使用点语法取出 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 内存管理原则

  1. 有对象的创建,就要匹配一个 release
  2. retian 的次数应与 release 的次数匹配
  3. 谁用谁 retain ,谁不用谁 release (看准操作的对象)
  4. 只有在多一处引用的时候才 retain,少一处引用的时候才 release

---> retain & release 要平衡


04 野指针与僵尸对象

  • C 语言中的野指针:在定义一个指针变量时,如果没有为其初始化,那么该指针存储的是一个垃圾值,指针指向一块随机的地址空间。

    OC 中的野指针:指针指向的对象已被系统回收

  • 内存回收的本质:变量被回收时,表示该变量原本占用的地址空间现在可以被重新分配出去。但在该地址空间被重新占用之前,其中存储的数据不会发生改变

    对象回收的本质:对象占用的空间可以被重新分配,但在分配出去之前,其中存储的对象数据还在。

  • 僵尸对象:一个已经被释放的对象,但这个对象所占用的空间可能还没有重新分配出去

    ---> 使用野指针来访问僵尸对象时,有可能有问题(已重新分配),也有可能没问题(还没重新分配)

    不建议访问僵尸对象 ---> 使用 Xcode 自动检查僵尸对象的机制(不设置则系统允许访问):

    1. 选择左上 停止符号 右侧的框 - edit scheme
    2. 选择 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 单个对象的内存泄露

几种可能发生单个对象的内存泄露的情况:

  1. 有对象的创建,但没有对应的 release

  2. retain & release 的次数不匹配

  3. 在不恰当的时候,为指向当前对象的指针赋值为 nil

  4. 在方法中为传入的对象进行不适当的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, 不会被回收
  • 如何保证单个对象可以被回收?
    1. 每创建一个新的对象,就必须为其匹配一个 release
    2. retain & release 的次数必须匹配
    3. 只有指针成为野指针时才将其赋值为 nil
    4. 在方法中要严谨使用 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 总结:如何处理类的属性是一个对象的情况

  1. 重写 setter 方法

    - (void)setCar:(Car *)car{
    if(_car != car){
    [_car release];
    _car = [car retian]; // 在给这种属性赋值时(不包括初始化),有一个 release 就要对应有一个 retain
    }
    }
  2. 重写 dealloc 方法

    - (void)dealloc{
    [_car release]; // 与 setter 方法中的 retain 呼应
    [super dealloc];
    }

注意:

  1. 注意内存管理范围:只有当类的属性为一个 OC 对象时,才需要这样特别处理 setter 和 dealloc
  2. 注意 NSString *,也是一个 OC 对象,也需要做上述处理

07 @property 参数(1)

7.1 @property 概述

【回顾】@property 作用:

  1. 自动生成私有属性

  2. 自动生成 setter & getter 方法的声明

  3. 自动生成 setter & getter 方法的实现

    --> setter 方法均是直接使用传入的参数赋值

问题:在 MRC 模式下,若想满足前面对 setter 方法的改动,应该如何使用 @property ?(不重写所有setter)
---> 令 @property 携带参数

语法格式:【 @property( 参数 1,参数 2 ,... ) 数据类型 名称 】 // 参数个数不限

@property 的四组参数:

  1. 与多线程相关的 2 个参数:atomic、nonatomic
  2. 与生成的 setter 方法的实现相关的 2 个参数:assign、retain
  3. 与生成 只读 / 读写 相关的 2 个参数:readonly、readwrite
  4. 与生成的 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 来自定义方法的名字

注意:

  1. setter 方法有参数,需要加 :

  2. 不影响点语法的使用。

    点语法内部实现原理:编译器编译时,会将点语法转化为调用 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 个步骤 ):

  1. 在其中一个类中,将对应另一个类的对象的属性声明改为 @property(..., assign, ...) Book *book;
    也就是在点语法调用 setter 方法时,只对一个对象使用 MRC 内存管理代码

  2. 修改对应类中的 dealloc 方法,删去其中的 [_book release]; 语句
    因为在声明属性时就没有把它当作 OC 对象处理,没有使用 retian --> retain & release 个数要匹配

posted @   Z/z  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示