今天回顾复习一下内存管理的知识点,发现了一个不可思议的问题,拿出来和大家一起分享。
在之前我们总是认为是这样的:
引用计数的工作原理:
1.当我们创建(alloc)一个新对象A的时候,它的引用计数从零变为 1;
2.当有一个指针指向这个对象A,也就是某对象想通过引用保留(retain)该对象A时,引用计数加 1;
3.当某个指针/对象不再指向这个对象A,也就是释放(release)该引用后,我们将其引用计数减 1;
4.当对象A的引用计数变为 0 时,说明这个对象不再被任何指针指向(引用)了,这个时候我们就可以将对象A销毁,所占内存将被回收,且所有指向该对象的引用也都变得无效了。
5.系统也会将其占用的内存标记为“可重用”(reuse);
操作引用计数的方法:
A.以下是NSObject协议中声明的3个用于操作计数器的方法:
retain : 保留。保留计数+1;
release : 释放。保留计数 -1;
autorelease :稍后(清理“自动释放池”时),再递减保留计数,所以作用是延迟对象的release;
B.dealloc方法:另外,当计数为0的时候对象会自动调用dealloc。而我们可以在dealloc方法做的,就是释放指向其他对象的引用,以及取消已经订阅的KVO、通知;(自己不能调用dealloc方法,因为运行期系统会在恰当的时候调用它,而且一旦调用dealloc方法,对象不再有效,即使后续方法再次调用retain。)
所以,调用release后会有2种情况:
调用前计数>1,计数减1;
调用前计数<1,对象内存被回收;
C.retainCount:获取引用计数的方法。
Eg: [object retainCount]; //得到object的引用计数
升级到Xcode8.0及以上版本之后
NSObject的用法还是和以前一样,但是NSString、NSArray、NSDictionary等就和之前有一点区别了,具体我们看看下面的代码进行一样验证
- (void) testRetainCount
{
NSString *str1 = [NSString stringWithFormat:@"a"];
[str1 retain];
NSLog(@"str1:%ld",[str1 retainCount]); // -1
NSString *str2 = [NSString stringWithFormat:@"abc1234567"];
NSLog(@"str2:%ld",[str2 retainCount]); // 1
//输出结果是 -1 ,1
//当format后面的字符串长度大于等于10时, 引用计数为1
NSString *str3 = [[NSString alloc] initWithString:@"abc1234567832143242314321424"];
NSLog(@"str3:%ld",[str3 retainCount]); // 输出结果:-1
//- (instancetype)initWithString:(NSString *)aString;
//如果 initWithString的字符串aString是一个存放在堆区的变量,alloc 之后,引用计数2
NSString *str4 = [[NSString alloc] initWithString:str2];
NSLog(@"retain之前str4:%ld",[str4 retainCount]); //输出结果:2
[str4 retain];
NSLog(@"retain之后str4:%ld",[str4 retainCount]); //输出结果:3
//如果 initWithString的字符串aString是一个常量,alloc 之后,引用计数还是-1
NSString *str5 = [[NSString alloc] initWithString:str3];
NSLog(@"retain之前str4:%ld",[str5 retainCount]); //输出结果:-1
[str5 retain];
NSLog(@"retain之后str5:%ld",[str5 retainCount]); //输出结果:-1
/*************NSArray*********************/
NSArray *array1 = [NSArray array];
NSLog(@"array1:%ld",[array1 retainCount]); //输出结果:-1
NSArray *array2 = [[NSArray alloc] initWithObjects:nil];
NSLog(@"array2:%ld",[array2 retainCount]); //输出结果:-1
NSArray *array3 = [[NSArray alloc] initWithObjects:@"", nil];
NSLog(@"array3:%ld",[array3 retainCount]); //输出结果:1
/*************NSDictionary*********************/
NSDictionary *dic1 = [NSDictionary dictionary];
NSLog(@"dic1:%ld",[dic1 retainCount]); // 输出结果:-1
NSDictionary *dic2 = [[NSDictionary alloc] initWithObjectsAndKeys:nil];
NSLog(@"dic2:%ld",[dic2 retainCount]); // 输出结果:-1
NSDictionary *dic3 = [[NSDictionary alloc] initWithObjectsAndKeys:@"",@"", nil];
NSLog(@"dic3:%ld",[dic3 retainCount]); // 输出结果:1
/*************NSObject*********************/
//NSObject还是和之前是一样的
NSObject *obj = [[NSObject alloc] init];
NSLog(@"obj:%ld",[obj retainCount]); //输出结果:1
}
输出结果:
综上所述:
我们忽略掉那个[str retain], 因为retain操作只会增加引用计数(相信系统, 相信Xcode). 那么为什么会是这样的打印结果?
首先需要明白的是, 引用计数机制只使用在堆中, 那么所有不保存在堆中的数据的引用计数都为-1.
在OC中, 几乎所有不可变对象(常量)都存在常量区(没有一一测试测, 有兴趣的可以试试), 内存管理由系统来做. 代码中的str系统默认是一个常量, 所以保存在常量区, 所以引用计数为-1. 哪怕是显式使用了alloc也同样不会保存在堆上, 所以引用计数还是-1.
但是, 既然常量保存在常量区, 那str1为什么引用计数为1?
这个, 私心想着(猜测), 所有可以有多个参数的初始化方法都有可能出现这种情况, 如果一个方法有多个参数, 那么方法内部就会有一个长度可变的数组保存参数(查看OC某些多参方法的声明, 出现va_list类型, 即可变数组类型), 因为长度不确定只能保存在堆中, 然后在方法执行过程中对数据处理后, 并没有将数据转存到栈中, 所以引用计数会+1;
亲测, 当format后面的字符串长度大于等于10时, 引用计数为1. 这个现象存在于initWithFormat:和stringWithFormat:方法, 至于有没有其他方法也会有这种情况不敢确定.
而在数组的初始化方法中initWithObjects:方法当参数为nil时引用计数为-1, 但是当有非nil参数的时候, 哪怕是空字符串@"", 引用计数都会成为1.
至于为什么会出现当参数长度大于某个值时才出现引用计数加一的情况, 可能是和栈内存分配有关, 因为不了解这个方面的细节, 就不多说了.