【iOS】——MRC

一、引用计数

内存管理的核心是引用计数器,用一个整数来表示对象被引用的次数,系统需要根据引用计数器来判断对象是否需要被回收。

在每次 RunLoop 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉。

引用计数器特点如下:

  • 每个对象都有引用计数器
  • 任何一个对象,刚创建的时候,初始的引用计数为 1
  • 没有任何对象持有该对象的引用计数时,也就是对象的引用计数为0,回收该对象
  • 如果对象的引用计数始终不为0,则始终不会被回收除非程序停止运行
对象操作对应方法
生成并持有对象alloc/new/copy/mutableCopy方法
持有对象retain方法
释放对象release方法
废弃对象dealloc方法
返回对象引用引用计数retainCount方法
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(@"--------");
    }

    return 0;
}

对象即将被销毁时会调用dealloc方法,因此可通过dealloc方法有没有被调用,就可以判断出对象是否被销毁

重写了 dealloc 方法,就必须调用 [super dealloc],并且放在最后面调用,重写dealloc方法应在类的实现中进行,不能直接调用 dealloc 方法

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

二、野指针和空指针

被释放的对象称为僵尸对象,此时对象不能被使用

指向僵尸对象的指针称为野指针

给一个野指针发送消息就会报错(EXC_BAD_ACCESS 错误)

没有指向存储空间的指针称为空指针(里面存的是 nil, 也就是 0)

在这里插入图片描述

为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针

三、内存管理思想

单个对象内存管理思想

  • 自己创建的对象自己持有

alloc\new\copy\mutableCopy 方法名开头来创建的对象意味着自己生成的对象只有自己持有。

id obj = [[NSObject alloc] init];

  • 不是自己创建的对象自己也能持有

用 alloc / new / copy / mutableCopy 以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。(比如 NSMutableArray 类的 array方法),但是我们可以通过retain来手动持有对象。

//取得的对象存在但不持有
id obj = [NSMutableArray array];
//持有该对象
[obj retain];

  • 不再需要自己持有的对象就将其释放

释放自己生成并持有的对象

//自己持有对象
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似可以访问,但对象一经释放绝对不可访问
[obj release];

释放非自己生成但持有的对象

id obj = [NSMutableArray array];
//持有该对象
[obj retain];

[obj release];

  • 无法释放不是自己持有的对象

如果不是自己持有的对象一定不能进行释放,倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。

多个对象内存管理思想

多个对象之间往往是通过 setter 方法产生联系的,通过setter方法让一个对象持有另一个对象的引用计数,其内存管理的方法也是在 setter 方法、dealloc 方法中实现的。

多个对象内存管理分为以下几种情况:

1.一个对象不持有另外一个对象

此种情况两个实例对象没有任何关联因此可以将各自释放掉

2.一个对象持有另外一个对象

此种情况通过setter方法将对象赋值给另外一个对象的成员变量,因此会让被持有对象的引用次数加1,所以就需要在setter方法中使用retain方法,为了保证该对象释放同时另外一个对象也能释放,再将另外一个对象引用计数减一

- (void)setMyObject:(MyObject*)myObj { 
    // 引用计数器 +1
    [myObj retain];
    _myObj = myObj;
}

当前对象要释放时因为还持有别的对象所以还要将持有的对象进行销毁

- (void)dealloc {
    // 人释放了, 那么房间也需要释放
    [_myObj release];
    NSLog(@"%s", __func__);
 
    [super dealloc];
}

3.一个对象持有并释放掉一个对象后持有另外一个对象

此种情况如果还使用先前的getter方法,不难发现将第一个对象赋值给该对象时第一个对象的引用计数加一,再将第一个对象引用计数减一后将第二个对象赋值给该对象时第二个对象的引用计数加一,再将第二个对象引用计数减一,释放该对象会让第二个对象的引用计数减一但是第一个对象则不会减一,因此会造成第一个对象无法被释放掉。

因此就需要修改setter方法每当我们需要更换持有的对象时就让原来持有的对象的引用计数减一。

- (void)setMyObject:(MyObject*)myObj { 
		[_myObj release];
    // 引用计数器 +1
    [myObj retain];
    _myObj = myObj;
}
4. 一个对象持有一个对象并释放该对象后再次持有该对象

此种情况使用上一个getter方法不难发现,我们将那个对象赋值给该对象后该对象对那个对象的引用计数加一,再将那个对象引用计数减一,此时两个对象的引用计数相同,此时再次进行赋值会导致那个对象的引用计数先减一此时为0变成了野指针,此时再对野指针进行retain操作就会报错。

此时就需要在setter方法中判断是否重复赋值,如果是同一个实例对象,就不需要重复进行 releaseretain

- (void)setMyObject:(MyObject*)myObj { 
   if (_myObj != myObj) {
   [_myObj release];
    // 引用计数器 +1
    [myObj retain];
    _myObj = myObj;
   }
}

因为 retain 不仅仅会对引用计数器 +1, 而且还会返回当前对象,所以上述代码可最终简化成:

- (void)setMyObject:(MyObject*)myObj { 
   if (_myObj != myObj) {
   [_myObj release];
    // 引用计数器 +1
    _myObj = [myObj retain];
   }
}

四、 @property参数

  • 在成员变量前加上 @property,系统就会自动帮我们生成基本的 setter / getter 方法,但是不会生成内存管理相关的代码。
@property (nonatomic) int val;
  • 如果在 property 后边加上 assign,系统也不会帮我们生成 setter 方法内存管理的代码,仅仅只会生成普通的 getter / setter 方法,默认什么都不写就是 assign
@property(nonatomic, assign) int val;
  • 如果在 property 后边加上 retain,系统就会自动帮我们生成 getter / setter 方法内存管理的代码,但是仍需要我们自己重写 dealloc 方法。
@property(nonatomic, retain)MyObject *myObj;

五、自动释放池

当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C 提供了 autorelease 方法。

autorelease 是一种支持引用计数的内存管理方式,主要用于延迟对象的释放,只要给对象发送一条 autorelease 消息,会将对象注册到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次 release 操作。

autorelease方法会返回对象本身,且调用完autorelease方法后,对象的计数器不变。

Person *p = [Person new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为 1

1.autoreleasepool的创建

autoreleasepool有两种创建方法

  • 第一种是使用 NSAutoreleasePool 创建
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 创建自动释放池
[pool release]; // [pool drain]; 销毁自动释放池

  • 第二种是使用 @autoreleasepool 创建
@autoreleasepool
{ // 开始代表创建自动释放池
 
} // 结束代表销毁自动释放池

2.autorelease 的使用方法

NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
Person *p = [[[Person alloc] init] autorelease];
[autoreleasePool drain];

需要注意的是对象调用autorelease方法需要在创建autoreleasepool之后否则无法将其注册到autoreleasepool。

@autoreleasepool
{ // 创建一个自动释放池
        Person *p = [[Person new] autorelease];
        // 将代码写到这里就放入了自动释放池
} // 销毁自动释放池(会给池子中所有对象发送一条 release 消息)

在自动释放池的外部发送 autorelease 不会被加入到自动释放池中。就算对象创建在自动释放池内没调用autorelease方法也不会将其注册到autoreleasepool。

@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
 
// 正确写法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }
 
// 正确写法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}

3.自动释放池的嵌套使用

  • 自动释放池是以栈的形式存在。

  • 由于栈只有一个入口,所以调用 autorelease 会将对象放到栈顶的自动释放池。

@autoreleasepool { // 栈底自动释放池
    @autoreleasepool {
        @autoreleasepool { // 栈顶自动释放池
            Person *p = [[[Person alloc] init] autorelease];
        }
        Person *p = [[[Person alloc] init] autorelease];
    }
}

4.自动释放池的注意事项

  • 自动释放池中不适宜放占用内存比较大的对象

因为自动释放池是延迟释放机制,只有清空池子时才会将注册到池子的对象销毁,此时如果有多个内存较大的对象就会造成短时间内内存暴涨。

  • 不要把大量循环操作放到同一个 @autoreleasepool 之间,这样会造成内存峰值的上升
// 内存暴涨
@autoreleasepool {
    for (int i = 0; i < 99999; ++i) {
        Person *p = [[[Person alloc] init] autorelease];
    }
}
// 内存不会暴涨
for (int i = 0; i < 99999; ++i) {
    @autoreleasepool {
        Person *p = [[[Person alloc] init] autorelease];
    }
}

两段代码的区别在于第一个自动释放池是在循环外部创建的,所有的Person对象都被添加到了同一个自动释放池中。它们并不会被立即释放。只有当自动释放池被排空时,也就当@autoreleasepool块执行完毕后,所有这些对象才会被release。因此,在循环结束前,所有创建的Person对象都存在于内存中,这会导致内存使用量显著增加,即所谓的“内存暴涨”。

第二个每次循环迭代时都会创建一个新的自动释放池。每一个Person对象都在创建它的那个迭代的自动释放池中被管理,并且在该迭代结束时,该自动释放池就会被排空,从而立即释放该迭代中创建的Person对象。由于每个Person对象都在其创建后的短时间内被释放,因此内存使用量不会像第一个例子那样累积,从而避免了“内存暴涨”的问题。

  • 不要连续调用 autorelease
@autoreleasepool {
 // 会导致过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }

  • 调用 autorelease 后又调用 release
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; //过度释放
}

六、循环引用

对象A和对象B互相作为对方的成员变量时也就是互相持有对方时相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。

循环引用也分为几种类型:

  1. 自循环引用
  2. 相互循环引用
  3. 多循环引用

1.自循环引用

假如有一个对象,内部强持有它的成员变量obj,若此时我们给obj赋值为原对象时,就是自循环引用。

2.相互循环引用

对象A内部强持有obj,对象B内部强持有obj,若此时对象A的obj指向对象B,同时对象B中的obj指向对象A,就是相互引用。

3.多循环引用

假如类中有对象1…对象N,每个对象中都强持有一个obj,若每个对象的obj都指向下个对象,就产生了多循环引用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/801053.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单链表算法 - 链表分割

链表分割_牛客题霸_牛客网现有一链表的头指针 ListNode* pHead&#xff0c;给一定值x&#xff0c;编写一段代码将所有小于x的。题目来自【牛客题霸】https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70思路: 代码: /* struct ListNode {int val;struct List…

烧掉一个亿就为造好一把椅子,西昊如何策划人体工学椅极致产品力

2011年到2015年这4年期间&#xff0c;功能单一&#xff0c;价值雷同的椅子市场趋于饱和&#xff0c;竞争的白热化和同质化带来了全行业的价格战&#xff0c;彼时西昊的经营陷入巨大的困境&#xff0c;所有的产品和钱都变成成库存&#xff0c;现金流几乎枯竭。因此&#xff0c;西…

文心一言《使用手册》,文心一言怎么用?

一、认识文心一言 &#xff08;一&#xff09;什么是文心一言 文心一言是百度研发的 人工智能大语言模型产品&#xff0c;能够通过上一句话&#xff0c;预测生成下一段话。 任何人都可以通过输入【指令】和文心一言进行对话互动、提出问题或要求&#xff0c;让文心一言高效地…

智能家居产品公司网站源码,自适应布局设计,带完整演示数据

适合各类智能家居电子产品使用的网站源码&#xff0c;深色大气设计&#xff0c;自适应布局设计&#xff0c;pc手机均可完美适配&#xff0c;带完整演示数据。 独家原创资源。源码是asp开发的&#xff0c;数据库是access&#xff0c;主流的虚拟主机空间都支持asp&#xff0c;直…

<Rust><GUI>rust语言GUI库tauri体验:前、后端结合创建一个窗口并修改其样式

前言 本文是rust语言下的GUI库&#xff1a;tauri来创建一个窗口的简单演示&#xff0c;主要说明一下&#xff0c;使用tauri这个库如何创建GUI以及如何添加部件、如何编写逻辑、如何修改风格等&#xff0c;所以&#xff0c;这也是一个专栏&#xff0c;将包括tauri库的多个方面。…

面对人工智能发展的伦理挑战:应对策略与未来方向

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

硬件开发笔记(二十六):AD21导入电感原理图库、封装库和3D模型

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140437290 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

SAP ERP与金蝶云星空的集成案例(新能源光伏行业)

一、项目环境 阳光新能源开发股份有限公司&#xff08;简称“阳光电源”&#xff09;是一家专注于新能源开发利用的国家高新技术企业。作为阳光电源&#xff08;股票代码&#xff1a;300274&#xff09;旗下的新能源开发投资平台&#xff0c;阳光新能源聚焦光伏、风电、风光…

暴雨让服务器不怕热҈热҈热҈热҈

在AI算力呈几何倍数增长的趋势下&#xff0c;算力逐渐朝着“高性能、高密度、高耗能“发展。在高耗能的算力下&#xff0c;AI服务器功率已逐步逼近风冷散热极限&#xff0c;而液冷作为更加高效、低能耗的制冷技术&#xff0c;逐渐成为了高密度算力散热场景的首选方案。 液冷的…

数据结构-java中链表的存储原理及使用方式

目录 链表&#xff08;线性表的链式存储&#xff09; 代码实例&#xff1a;&#xff08;链表构建&#xff0c;头插尾插&#xff09; LinkedList LinkedList的使用&#xff1a; 1、构造方法 2、操作方法 LinkedList 和 ArrayList 的区别 链表&#xff08;线性表的链式存储…

Minio搭建文件服务器的学习

MinIO是一个高性能的开源对象存储服务器&#xff0c;与Amazon S3兼容。它使用Go语言编写&#xff0c;可以在多种操作系统上运行&#xff0c;如Linux、MacOS和Windows等。MinIO的分布式特性使其能够轻松扩展存储容量和处理能力&#xff0c;满足大规模数据存储的需求。 使用Docke…

你也想做一个Element-ui吧!!!——Blueの前端路(一)

目录 前言&#xff1a; 父子组件 button组件 使用vue脚手架初始化一个项目 如何封装&#xff0c;注册和使用一个组件 main.js中将组件设置为全局 使用 此组件我们所需实现的内容 type 父组件组件传递type属性 子组件接收负组件传递的数据 通过绑定类名的方法动态控制…

北汽蓝谷:预期能否兑现

天天提热搜&#xff0c;公司却一路亏损&#xff0c;股价却一路走高&#xff0c;今天说说——北汽蓝谷 问最近车圈谁最热&#xff0c;北汽蓝谷少不了。先是享界S9上市定档&#xff0c;华为余承东“在线带货”。 后有百度无人驾驶萝卜快跑引发全网热议&#xff0c;所用车型便来自…

TF/SD卡开发驱动(SPI)

TF与SD卡本质上来说都是flash类型的存储器 可以理解为TF卡是SD卡的升级版&#xff0c;体积小功能强大&#xff0c;SD卡是传统意义上的存储卡&#xff0c;适用范围比较广&#xff0c;而SD卡的驱动方式有两种 SDIO 和 SPI&#xff0c;同理TF卡也是一样 &#xff08;在资源足够…

2024黑马AI+若依框架项目开发 个人心得、踩坑和bug记录 全网最快最全 基础功能认识篇

2024黑马AI若依框架项目开发 个人心得、踩坑和bug记录 全网最快最全 基础功能认识篇 你好,我是Qiuner. 为帮助别人少走弯路和记录自己编程学习过程而写博客 这是我的 github https://github.com/Qiuner ⭐️ ​ gitee https://gitee.com/Qiuner &#x1f339; 如果本篇文章帮到…

[web]-图片上传、文件包含-图片上传

题目内容提示&#xff1a;上传图片试试吧&#xff0c;注意统一时区问题 打开页面如图&#xff0c;源码没有过滤&#xff0c;随便输入&#xff0c;进入上传目录 根据链接可以看到是文件包含&#xff0c;可以利用编码读取源码&#xff0c;这里只列出有用页面的编码&#xff08;?…

初识C++|类和对象(中)——类的默认成员函数

&#x1f36c; mooridy-CSDN博客 &#x1f9c1;C专栏&#xff08;更新中&#xff01;&#xff09; &#x1f379;初始C|类与对象&#xff08;上&#xff09;-CSDN博客 4. 类的默认成员函数 默认成员函数就是⽤⼾没有显式实现&#xff0c;编译器会⾃动⽣成的成员函数称为默认成…

测试开发面经总结(三)

TCP三次握手 TCP 是面向连接的协议&#xff0c;所以使用 TCP 前必须先建立连接&#xff0c;而建立连接是通过三次握手来进行的。 一开始&#xff0c;客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口&#xff0c;处于 LISTEN 状态 客户端会随机初始化序号&…

简单了解线程池

线程池 线程池的概念线程池的优势 线程池属性介绍线程池的使用简单实现线程池总结 线程池的概念 线程池(ThreadPoolExecutor) 顾名思义&#xff0c;在一个“池”中存放多个线程。 与常量池、数据库连接池等思想是一样的&#xff0c;为的都是提高效率。 我们已经领教了多线程的…

python关于excel常用函数(pandas篇)

iterrows函数&#xff1a; Pandas的基础数据结构可以分为两种&#xff1a;DataFrame和Series。不同于Series的是&#xff0c;Dataframe不仅有行索引还有列索引 。df.iterrows( )函数&#xff1a;可以返回所有的行索引&#xff0c;以及该行的所有内容。 pd.read_excel&#xf…