常常面试的时候,会被问到“谈谈你对OC中内存管理的理解”,个人觉得应该从以下三个部分来回答,才比较全面
本文主要介绍OC的内存管理的模式(机制)来分析。
所以,我们需要对内存进行合理的分配内存、清除内存,回收那些不需要再使用的对象。从而保证程序的稳定性。
任何继承了NSObject的对象需要进行内存管理,而其他非对象类型(int、char、float、double、struct、enum等) 不需要进行内存管理
这是因为:
在OC中没有垃圾回收机制,OC提供了一套机制来管理内存,即“引用计数”,每个OC对象都有自己的引用计数器
《iOS与OS X多线程和内存管理》这本书说的是
但是个人觉得太绕了,简单来说: 对于所有的对象而言,你只要记住Apple的官网上的内存管理三定律就可以:
在开发时引用计数又分为ARC(自动引用计数)和MRC(手动引用计数)。ARC的本质其实就是MRC,只不过是系统帮助开发者管理已创建的对象或内存空间,自动在系统认为合适的时间和地点释放掉已经失去作用的内存空间,原理是一样的。
而对于自动释放池(Autorelease Pool可以算是半自动的机制,所以这里我单独归为一类,不和MRC一起分析。
遵循谁申请、谁添加、谁释放的原则。需要手动处理内存技术的增加和修改。将从以下几个方面来深入了解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被释放了”****
在MRC中会引起引用计数变化的关键字有:alloc,retain,copy,release,autorelease。(strong关键字只用于ARC,作用等同于retain)
MRC中常用的属性关键自主要是:assign、reatin、copy
@property (nonatomic,assign) int val; 复制代码
@property (nonatomic,copy) NSString *name; // copy修饰的属性,内部setter方法的实现大概酱紫: - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name copy]; } } 复制代码
@property (nonatomic,retain) NSString *name; // retain修饰的属性,内部setter方法的实现大概酱紫: - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } } 复制代码
我门先来弄清楚一些概念:
iOS提供了两个拷贝方法:
拷贝的目的
浅拷贝和深拷贝
思考🤔:对于copy,引用计数是否可能小于0?
下面来看几个列子:
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 复制代码
在App编译阶段,由Xcode添加了内存管理的代码,自动加入了 retain 、 release 后的代码
只要没有强指针指向(没有被强引用),对象就会被释放。
Person *person1 = [[Person alloc] init]; __strong Person *person2 = [[Person alloc] init]; 复制代码
相应的也有弱指针(弱引用),但不影响对象的释放
__weak Person *p = [[Person alloc] init]; 复制代码
简单看几种情况:
#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] ----自动释放池已释放------ 复制代码
它不会像ARC或者MRC那样在对象不再会被使用时马上被释放,而是等到一个时机去释放它,内存池的释放操作分为自动和手动。自动释放受runloop机制影响
比如for循环,可能是内存飙升,这个时候我门可以手动释放内存(@autoreleasepool)
// ARC NSMutableArray * arr = [NSMutableArray array]; for (int i = 0; i < largeCount; i++) { @autoreleasepool {//开始代表创建自动释放池 NSNumber * numTep = [NSNumber numberWithInt:i]; [arr addObject:numTep]; }//结束代表销毁自动释放池 } 复制代码
方式一:
NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init]; Person *p = [[[Person alloc] init] autorelease]; [autoreleasePool drain]; 复制代码
方式二:
@autoreleasepool { // 创建一个自动释放池 Person *p = [[Person new] autorelease]; // 将代码写到这里就放入了自动释放池 } // 销毁自动释放池(会给池子中所有对象发送一条release消息) 复制代码
这里涉及到runloop,不做详细的描述,会在runloop相关文章中在做深入讲解,有个大概了解即可
苹果在主线程 RunLoop 里注册了两个 Observer:
当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法,在MRC中才能使用。
//错误的写法 @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]; } 复制代码
参考文章: