内存管理系列—OC的内存管理模式
2020/3/25 23:01:48
本文主要是介绍内存管理系列—OC的内存管理模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
1. 引言
常常面试的时候,会被问到“谈谈你对OC中内存管理的理解”,个人觉得应该从以下三个部分来回答,才比较全面
本文主要介绍OC的内存管理的模式(机制)来分析。
2.为什么需要对OC进行内存管理
- 程序在运行的过程中通常通过以下行为,来增加程序的的内存占用
- 创建一个OC对象
- 定义一个变量
- 调用一个函数或者方法
- 移动设备分配给每个App的内存有限,App运行中会创建大量对象, OC对象存储在堆中,系统不会自动释放堆中的内存,对象没有及时释放,就会占用大量内存,系统会发出内存警告,对应用运行造成影响
- 如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验
所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。
3. 哪些对象才需要我们进行内存管理
任何继承了NSObject的对象需要进行内存管理,而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理
这是因为:
- 继承了NSObject的对象的存储在操作系统的堆里边。一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
- 非OC对象一般放在操作系统的栈里面,由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进后出)
4. 内存管理机制
在OC中没有垃圾回收机制,OC提供了一套机制来管理内存,即“引用计数”,每个OC对象都有自己的引用计数器
4.1 引用计数
- 【简单定义】:引用计数简单来说就是统计一块内存的所有权, 当这块内存被创建出来的时候(alloc、new或copy),它的引用计数从0增加到1,表示有一个对象或指针持有这块内存,拥有这块内存的所有权
- 【引用计数的增加】:如果这时候有另外一个对象或指针指向这块内存,那么为了表示这个后来的对象或指针对这块内存的所有权,引用计数加1变为2
- 【引用计数的较少】:之后若有一个对象或指针不再指向这块内存时,引用计数减1,表示这个对象或指针不再拥有这块内存的所有权
- 【内存的释放】:当一块内存的引用计数变为0,表示没有任何对象或指针持有这块内存,系统便会立刻释放掉这块内存
5. 内存管理原则
《iOS与OS X多线程和内存管理》这本书说的是
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不在需要自己持有的对象时释放
- 非自己持有的对象无法释放
但是个人觉得太绕了,简单来说: 对于所有的对象而言,你只要记住Apple的官网上的内存管理三定律就可以:
- 一个对象可以有一个或多个拥有者
- 当它一个拥有者都没有时,它就会被回收
- 如果想保留一个对象不被回收,你就必需成为它的拥有者(ownership)
6. 内存管理的3种模式
在开发时引用计数又分为ARC(自动引用计数)和MRC(手动引用计数)。ARC的本质其实就是MRC,只不过是系统帮助开发者管理已创建的对象或内存空间,自动在系统认为合适的时间和地点释放掉已经失去作用的内存空间,原理是一样的。
而对于自动释放池(Autorelease Pool可以算是半自动的机制,所以这里我单独归为一类,不和MRC一起分析。
6.1 MRC手动引用计数
遵循谁申请、谁添加、谁释放的原则。需要手动处理内存技术的增加和修改。将从以下几个方面来深入了解MRC种的内存管理。
6.1.1 内存管理原则
- 谁创建谁release:如果你通过alloc、new、copy或mutableCopy来创建一个对象,那么必须调用release或autorelease
- 谁retain谁release: 只要你调用了retain,就必须调用一次release
上个demo感受下
运行环境:MRC模式
-(void)test{ @autoreleasepool { Person *p = [[Person alloc] init]; NSLog(@"retainCount = %lu", [p retainCount]); // 1 [p retain];// 只要给对象发送一个retain消息, 对象的引用计数器就会+1 NSLog(@"retainCount = %lu", [p retainCount]); // 2 // 通过指针变量p,给p指向的对象发送一条release消息 // 只要对象接收到release消息, 引用计数器就会-1 [p release]; // 需要注意的是: release并不代表销毁\回收对象, 仅仅是计数器-1 NSLog(@"retainCount = %lu", [p retainCount]); // 1 [p release]; // 0 } NSLog(@"----自动释放池已释放------"); } // 打印结果 2020-03-25 14:58:10.251949+0800 02-内存管理-MRC开发[13803:235592] retainCount = 1 2020-03-25 14:58:10.252081+0800 02-内存管理-MRC开发[13803:235592] retainCount = 2 2020-03-25 14:58:10.252147+0800 02-内存管理-MRC开发[13803:235592] retainCount = 1 2020-03-25 14:58:10.252239+0800 02-内存管理-MRC开发[13803:235592] Person被释放了 2020-03-25 14:58:10.252332+0800 02-内存管理-MRC开发[13803:235592] ----自动释放池已释放------ 复制代码
当引用计数为0的时候,person对象就被释放了,Peson中得dealloc方法就会打印“Person被释放了”****
6.1.2 引用计数关键字
在MRC中会引起引用计数变化的关键字有:alloc,retain,copy,release,autorelease。(strong关键字只用于ARC,作用等同于retain)
- alloc:它的作用是开辟一块新的内存空间,并使这块内存的引用计数从0增加到1,
- retain:只能由对象调用,它的作用是使这个对象的内存空间的引用计数加1,并不会新开辟一块内存空间
- copy:这个部分单独抽出来讲,涉及内容比较多。
- release:它的作用是使对象的内存空间的引用计数减1,若引用计数变为0则系统会立刻释放掉这块内存。如果引用计数为0的基础上再调用release,便会造成过度释放,使内存崩溃;
- autorelease:它的作用于release类似,但不是立刻减1,相当于一个延迟的release,通常用于方法返回值的释放
6.1.3 属性关键字
MRC中常用的属性关键自主要是:assign、reatin、copy
- assign
如果在property后边加上assign,系统就不会帮我们生成set方法内存管理的代码,仅仅只会生成普通的getter/setter方法,默认什么都不写就是assign
@property (nonatomic,assign) int val; 复制代码
- copy关键字
系统会自动帮我们生成getter/setter方法内存管理的代码
@property (nonatomic,copy) NSString *name; // copy修饰的属性,内部setter方法的实现大概酱紫: - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name copy]; } } 复制代码
- retain关键字
系统会自动帮我们生成getter/setter方法内存管理的代码
@property (nonatomic,retain) NSString *name; // retain修饰的属性,内部setter方法的实现大概酱紫: - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } } 复制代码
6.1.4 copy关键字重点解析
我门先来弄清楚一些概念:
-
iOS提供了两个拷贝方法:
- copy 不可变拷贝,产生【不可变】副本(即便本来是可变的,拷贝出来的都是不可变的)
- mutableCopy 可变拷贝,产生【可变】副本(即便本来是不可变的,拷贝出来的都是可变的)
-
拷贝的目的
- 产生一个副本对象,跟源对象互不影响
- 修改了源对象,不会影响副本对象
- 修改了副本对象,不会影响源对象
-
浅拷贝和深拷贝
- 浅拷贝:指针拷贝,【没有】产生新对象,引用计数会加1(retainCount += 1)
- 深拷贝:内容拷贝,【有】产生新对象,引用计数初始1(retainCount = 1)
思考🤔:对于copy,引用计数是否可能小于0?
下面来看几个列子:
- TaggedPointer的浅拷贝和深拷贝
void copyTest1) { NSString *str1 = [[NSString alloc] initWithFormat:@"abc"]; // TaggedPointer NSString *str2 = str1.copy; // 浅拷贝 TaggedPointer NSMutableString *str3 = str1.mutableCopy; // 深拷贝 对象 NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1); NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2); NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3); } // 打印结果 2020-03-25 16:00:37.509103+0800 03-内存管理-copy[14479:285375] str1 abc --- -1 --- 0x8093262be428885 2020-03-25 16:00:37.509190+0800 03-内存管理-copy[14479:285375] str2 abc --- -1 --- 0x8093262be428885 2020-03-25 16:00:37.509270+0800 03-内存管理-copy[14479:285375] str3 abc --- 1 --- 0x1007029d0 复制代码
void copyTest2() { NSString *str1 = @"ABC"; // 直接写出来的,不是通过方法创建的字符串,编译时会生成为【字符串常量】 NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1); NSString *str2 = str1.copy; // 浅拷贝 字符串常量 NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2); NSString *str3 = [[NSString alloc] initWithFormat:@"efg"]; // TaggedPointer NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3); NSMutableString *str4 = str3.mutableCopy; // 深拷贝 对象 NSString *str5 = str4.copy; // 深拷贝 对象 NSLog(@"str4 %@ --- %zd --- %p", str4, str4.retainCount, str4); NSLog(@"str5 %@ --- %zd --- %p", str5, str5.retainCount, str5); //打印结果 2020-03-25 16:12:07.188709+0800 03-内存管理-copy[14642:295736] str1 ABC --- -1 --- 0x1000030b0 2020-03-25 16:12:07.188866+0800 03-内存管理-copy[14642:295736] str2 ABC --- -1 --- 0x1000030b0 2020-03-25 16:12:07.189023+0800 03-内存管理-copy[14642:295736] str3 efg --- -1 --- 0xd9de73a142fc7bc1 2020-03-25 16:12:07.189245+0800 03-内存管理-copy[14642:295736] str4 efg --- 1 --- 0x10077d8d0 2020-03-25 16:12:07.189316+0800 03-内存管理-copy[14642:295736] str5 efg --- -1 --- 0xd9de73a142fc7bc1 } 复制代码
总结:
对于常量区的数据(字符串常量),TaggedPointer的引用计数一直都为【-1】,TaggedPointer不是对象,是个指针。 复制代码
思考🤔:对于copy,引用计数是否可能大于1,网上很多文章说copy不会改变引用计数?
void copyTest3() { NSString *str1 = [[NSString alloc] initWithFormat:@"老郑的技术杂货铺"]; // 对象 str1.retainCount = 1 NSString *str2 = str1.copy; // 浅拷贝 对象 str1.retainCount = 2 NSMutableString *str3 = str1.mutableCopy; // 深拷贝 对象 str3.retainCount = 1 NSLog(@"str1 %@ --- %zd --- %p", str1, str1.retainCount, str1); NSLog(@"str2 %@ --- %zd --- %p", str2, str2.retainCount, str2); NSLog(@"str3 %@ --- %zd --- %p", str3, str3.retainCount, str3); } //打印结果: 2020-03-25 16:18:45.498256+0800 03-内存管理-copy[14728:302195] str1 老郑的技术杂货铺 --- 2 --- 0x1006059b0 2020-03-25 16:18:45.498313+0800 03-内存管理-copy[14728:302195] str2 老郑的技术杂货铺 --- 2 --- 0x1006059b0 2020-03-25 16:18:45.498349+0800 03-内存管理-copy[14728:302195] str3 老郑的技术杂货铺 --- 1 --- 0x100605820 复制代码
总结
可见对不可变对象进行copy操作,引用计数会+1 复制代码
6.2 ARC自动引用计数管理
在App编译阶段,由Xcode添加了内存管理的代码,自动加入了 retain 、 release 后的代码
6.2.1 内存管理原则
只要没有强指针指向(没有被强引用),对象就会被释放。
- 强指针
- 默认所有对象的指针变量都是强指针
- 被__strong修饰的指针
Person *person1 = [[Person alloc] init]; __strong Person *person2 = [[Person alloc] init]; 复制代码
相应的也有弱指针(弱引用),但不影响对象的释放
- 弱指针:被__weak修饰的指针
__weak Person *p = [[Person alloc] init]; 复制代码
6.2.2 属性关键字
- strong : 用于OC对象,相当于MRC中的retain,是强引用
- weak : 用于OC对象,相当于MRC中的assign,比如修饰delegate
- assign : 用于基本数据类型,跟MRC中的assign一样,也可修饰对象,但会存在野指针
- copy : 主要用于修饰block、字符串、数组
6.2.3 ARC注意事项
- 不允许调用release、retain、retainCount、autorelease方法
- 重写父类的dealloc方法时,不能再调用 [super dealloc];
6.2.4 ARC中对象的释放
简单看几种情况:
- 局部变量释放对象随之被释放
#import "Person.h" @implementation Person -(void)dealloc{ NSLog(@"Person已释放-----dealloc"); } @end 复制代码
//Test.m -(void)test{ @autoreleasepool { int a = 10; // 栈 int b = 20; // 栈 //p在栈上 Person对象(计数器==1) : 堆 Person *p = [[Person alloc] init]; }// 执行到这一行局部变量p释放 // 由于没有强指针指向对象, 所以对象也释放 NSLog(@"----自动释放池已释放------"); } // 打印结果 2020-03-25 16:32:39.073845+0800 Test[14845:312200] Person已释放-----dealloc 2020-03-25 16:32:39.073965+0800 Test[14845:312200] ----自动释放池已释放------ 复制代码
- 清空指针对象随之被释放
-(void)test{ @autoreleasepool { Person *p = [[Person alloc] init]; p = nil; // 执行到这一行, 由于没有强指针指向对象, 所以对象被释放 NSLog(@"----当前还在函数作用域内------"); } NSLog(@"----自动释放池已释放------"); } // 打印结果 2020-03-25 16:39:12.924164+0800 Test[14915:317802] Person已释放-----dealloc 2020-03-25 16:39:12.924311+0800 Test[14915:317802] ----当前还在函数作用域内------ 2020-03-25 16:39:12.924446+0800 Test[14915:317802] ----自动释放池已释放------ 复制代码
6.3 自动释放池(Autorelease Pool)
它不会像ARC或者MRC那样在对象不再会被使用时马上被释放,而是等到一个时机去释放它,内存池的释放操作分为自动和手动。自动释放受runloop机制影响
6.3.1 手动释放
- ARC下使用方式:
比如for循环,可能是内存飙升,这个时候我门可以手动释放内存(@autoreleasepool)
// ARC NSMutableArray * arr = [NSMutableArray array]; for (int i = 0; i < largeCount; i++) { @autoreleasepool {//开始代表创建自动释放池 NSNumber * numTep = [NSNumber numberWithInt:i]; [arr addObject:numTep]; }//结束代表销毁自动释放池 } 复制代码
- MRC下使用方式
方式一:
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; Person *p = [[[Person alloc] init] autorelease]; [autoreleasePool drain]; 复制代码
方式二:
@autoreleasepool { // 创建一个自动释放池 Person *p = [[Person new] autorelease]; // 将代码写到这里就放入了自动释放池 } // 销毁自动释放池(会给池子中所有对象发送一条release消息) 复制代码
6.3.2 自动释放
这里涉及到runloop,不做详细的描述,会在runloop相关文章中在做深入讲解,有个大概了解即可
苹果在主线程 RunLoop 里注册了两个 Observer:
- 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
- BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
- Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后
6.3.3 autorelease关键字和释放池(MRC)
当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法,在MRC中才能使用。
- autorelease注意事项
- autorelease是一种支持引用计数的内存管理方式,只要给对象发送一条autorelease消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次release操作
- autorelease方法会返回对象本身,且调用完autorelease方法后,对象的计数器不变
- 并不是放到自动释放池代码中,都会自动加入到自动释放池
- 在自动释放池的外部发送autorelease 不会被加入到自动释放池中
- 错误写法❌
//错误的写法 @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]; } 复制代码
- autorelease本质原理
autorelease实际上只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中的所有对象会被调用release。
6.3.4 小结
- autorelease方法不会改变对象的引用计数器(销毁时影响),只是将这个对象放到自动释放池中;
- OC中类库的类方法一般都不需要手动释放,因为内部已经调用了autorelease方法,如[NSDate date]
- 自动释放池是以栈的形式存在
- 自动释放池中不适宜放占用内存比较大的对象
参考文章:
这篇关于内存管理系列—OC的内存管理模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-10-05Swift语法学习--基于协议进行网络请求
- 2022-08-17Apple开发_Swift语言地标注释
- 2022-07-24Swift 初见
- 2022-05-22SwiftUI App 支持多语种 All In One
- 2022-05-10SwiftUI 组件参数简写 All In One
- 2022-04-14SwiftUI 学习笔记
- 2022-02-23Swift 文件夹和文件操作
- 2022-02-17Swift中使用KVO
- 2022-02-08Swift 汇编 String array
- 2022-01-30SwiftUI3.0页面反向传值