经常面试的时候,会被问到“谈谈你对OC中内存管理的理解”,我的以为应该从如下三个部分来回答,才比较全面程序员
本文主要介绍OC的内存管理的模式(机制)来分析。 面试
因此,咱们须要对内存进行合理的分配内存、清除内存,回收那些不须要再使用的对象。从而保证程序的稳定性。数组
任何继承了NSObject的对象须要进行内存管理,而其余非对象类型(int、char、float、double、struct、enum等) 不须要进行内存管理bash
这是由于:数据结构
在OC中没有垃圾回收机制,OC提供了一套机制来管理内存,即“引用计数”,每一个OC对象都有本身的引用计数器多线程
《iOS与OS X多线程和内存管理》这本书说的是函数
可是我的以为太绕了,简单来讲: 对于全部的对象而言,你只要记住Apple的官网上的内存管理三定律就能够:oop
在开发时引用计数又分为ARC(自动引用计数)和MRC(手动引用计数)。ARC的本质其实就是MRC,只不过是系统帮助开发者管理已建立的对象或内存空间,自动在系统认为合适的时间和地点释放掉已经失去做用的内存空间,原理是同样的。布局
而对于自动释放池(Autorelease Pool能够算是半自动的机制,因此这里我单独归为一类,不和MRC一块儿分析。post
遵循谁申请、谁添加、谁释放的原则。须要手动处理内存技术的增长和修改。将从如下几个方面来深刻了解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];
}
复制代码
参考文章: