IOS之MRC ARC
作者是以前搞Android的,用的是java语言,对象的释放都是由虚拟机完成,IOS用的是Object C对象需要开发者自己管理MRC(Mannul Reference Counting),自己创建的对象需要自己释放,之后因为开发者太容易遗忘释放,导致出错,所以出现了ARC(Automic Reference Counting)机制,即系统自动管理机制,其实就是在OC编译的时候插入一些内存管理的代码,比如说retain release之类的
提到MRC和ARC必然要提到引用计数,即retainCount,如果一个对象的有引用计数是0,那么该对象就会被系统回收,释放内存空间
废话不多说直接上代码
#ViewController.m ARC环境下编译
#define RC(obj) CFGetRetainCount((__bridge CFTypeRef)(obj))
__weak NSDictionary *testWeak1 = nil;
__weak NSDictionary *testWeak2 = nil;
__weak NSDictionary *testWeak3 = nil;
__weak NSDictionary *testWeak4 = nil;
__weak NSDictionary *testWeak5 = nil;
@implementation ViewController
- (void)handleGesture
{
[self testReference];
[self testWeak];
}
- (void)testReference{
TestARC *arc = [[TestARC alloc]init];
TestMRC *mrc1 = [[TestMRC alloc]init];
NSDictionary *adExtraDic1 = [mrc1 copyTest];
NSDictionary *adExtraDic2 = [mrc1 testReturnDic];
NSDictionary *adExtraDic3 = [arc testReturnDic];
NSDictionary *adExtraDic4 = [arc copyTestARCDic];
//NSDictionary *adExtraDic5 = [[NSDictionary alloc]init];
NSDictionary *adExtraDic5 = @{@"pull_time": @(1),
@"pull_time_1": @(2)
};
//RC是在ARC模式下,对象的引用计数方法
NSLog(@"viewDidLoad 111 adExtraDic1 111 count = %ld", RC(adExtraDic1));
NSLog(@"viewDidLoad 111 adExtraDic2 111 count = %ld", RC(adExtraDic2));
NSLog(@"viewDidLoad 111 adExtraDic3 000 count = %ld", RC(adExtraDic3));
NSLog(@"viewDidLoad 111 adExtraDic4 000 count = %ld", RC(adExtraDic4));
NSLog(@"viewDidLoad 111 adExtraDic5 000 count = %ld", RC(adExtraDic5));
testWeak1 = adExtraDic1;
testWeak2 = adExtraDic2;
testWeak3 = adExtraDic3;
testWeak4 = adExtraDic4;
testWeak5 = adExtraDic5;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"testReference delay testWeak1 = %p,testWeak2=%p,testWeak3=%p,testWeak4=%p,testWeak5=%p", testWeak1,testWeak2,testWeak3,testWeak4,testWeak5);
135. });
136. }
- (void)testWeak{
NSLog(@"testWeak testWeak1 = %p,testWeak2=%p,testWeak3=%p,testWeak4=%p,testWeak5=%p", testWeak1,testWeak2,testWeak3,testWeak4,testWeak5);
}
#TestMRC.m MRC环境下编译
-(NSDictionary *)copyTest
{
NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
[adExtraDic setValue:@("abcd") forKey:@"c2s_switch"];
return adExtraDic;
}
-(NSDictionary *) testReturnDic
{
NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
[adExtraDic setValue:@("abcd") forKey:@"c2s_switch"];
return [adExtraDic autorelease];
}
- (void)dealloc
{
NSLog(@"TestMRC dealloc obj=%p",self);
}
#TestARC.m ARC环境下编译
49.-(NSDictionary *) testReturnDic
50.{
51. NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
52. return adExtraDic ;
53.}
-(NSDictionary *) copyTestARCDic
{
NSMutableDictionary *adExtraDic = [[NSMutableDictionary alloc] init];
// NSLog(@"TestARC copyTestARCDic adExtraDic=%p",adExtraDic);
return adExtraDic ;
}
- (void)dealloc
{
NSLog(@"TestARC dealloc");
}
打印结果
2019-09-23 16:31:33.574273+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic1 111 count = 1
2019-09-23 16:31:33.574285+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic2 111 count = 2
2019-09-23 16:31:33.574293+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic3 000 count = 1
2019-09-23 16:31:33.574302+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic4 000 count = 1
2019-09-23 16:31:33.574319+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic5 000 count = 2
2019-09-23 16:31:33.574382+0800 TestMac[46711:3554859] viewDidLoad 111 adExtraDic7 = __NSDictionaryM adExtraDic6=__NSFrozenDictionaryM
2019-09-23 16:31:33.574414+0800 TestMac[46711:3554859] handleGesture testWeak1 = 0x6000002e3620,testWeak2=0x6000002e35e0,testWeak3=0x6000002e3640,testWeak4=0x6000002e3660,testWeak5=0x6000017b4440
2019-09-23 16:31:33.581273+0800 TestMac[46711:3554859] handleGesture testWeakObj0 = 0x60000000e980
2019-09-23 16:31:33.581358+0800 TestMac[46711:3554859] TestNSObject dealloc obj=0x60000000e980
2019-09-23 16:31:33.581381+0800 TestMac[46711:3554859] TestMRC dealloc
2019-09-23 16:31:33.581393+0800 TestMac[46711:3554859] TestARC dealloc
2019-09-23 16:31:33.581418+0800 TestMac[46711:3554859] TestNSObject dealloc obj=0x60000000e8b0
2019-09-23 16:31:33.581438+0800 TestMac[46711:3554859] testWeak testWeak1 = 0x0,testWeak2=0x6000002e35e0,testWeak3=0x0,testWeak4=0x0,testWeak5=0x6000017b4440
2019-09-23 16:31:35.581492+0800 TestMac[46711:3554859] testReference delay testWeak1 = 0x0,testWeak2=0x0,testWeak3=0x0,testWeak4=0x0,testWeak5=0x0
下面来一个个分析下
PS:汇编代码和源码已上传,可以对照汇编和源码的行号来理解汇编语言
先看第1行和第2行日志 都是mrc为什么一个是1,一个是2
先来看第2行日志,直接上汇编语言吧
TesrMRC中testReturnDic的汇编如下
testReturnDic会调用
[[NSMutableDictionary alloc] init];这个会让对象的retainCount+1,此时对象的引用计数是1
再来看看ViewControll中调用[mrc1 testReturnDic]的汇编语言
在文章的开头说过,ARC会在编译的时候插入一些内存管理的代码,这里就可以看到
callq _objc_retainAutoreleasedReturnValue
这句话是什么意思呢,其实就是尝试retain一个return的value,为什么是尝试,这里不说,稍后再说
看看_objc_retainAutoreleasedReturnValue的源码
1.id objc_retainAutoreleasedReturnValue(id obj)
2. {
3. if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
4 . return objc_retain(obj);
5.}
先忽略第3行代码,直接跳到第4行代码,这里做了一次retain,引用计数加+1,此时对象的引用计数是2,所以说第2行打印的日志是2
在回过头来看看第一行日志
TesrMRC中copyTest的汇编如下
image.png
跟上面的testReturnDic的汇编差不多,这里不多说,主要看看
ViewControll中调用[mrc1 copyTest]的汇编语言
image.png
看上去很简单,并没有调用_objc_retainAutoreleasedReturnValue的代码
为什么会这样同样是MRC,几乎同样的函数实现,就是函数名不一样
这是因为根据苹果的命名规定,调用以alloc/new/copy/mutableCopy等开头的方法,表示调用者自己生成并持有对象,所以不需要retian,即在编译器识别这些方法时,不会自动加上_objc_retainAutoreleasedReturnValue,所以对应的引用计数是1
再来看看第三行日志,即[arc testReturnDic]对应的引用计数
是1,为什么呢?再一次看看arc testReturnDic对应的汇编语言
image.png
可以看出这个明显要比[mrc1 testReturnDic]的汇编语言要复杂一些,这也就说明了ARC机制会在编译的时候自动插入了一些代码来自动管理内存,下面来看看主要插入了哪些代码
312-316行是函数调用,在ios中函数调用实际就是消息机制,所以都是_objc_msgSend开头的
,这里面两个_objc_msgSend对应的就是alloc和init函数
继续往下看321有个retain调用,这就是ARC自动插入来的代码增加引用计数,这里对应的代码
是return adExtraDic ;其实可以理解为
NSMutableDictionary *adExtraDic1 = adExtraDic;
return adExtraDic1;
这样会更容易理解插入retain的代码
继续看汇编语言的第53行调用了一个
_objc_storeStrong这行汇编对应的是函数结束的位置代码53行,可以认为是栈帧出栈,需要释放局部变量,objc_storeStrong的实现如下
image.png
此时经调试runtime发现obj为nil,prev就是adExtraDic,第10行,调用了release(prev),在这里objc_storeStrong就是释放adExtraDic的,即局部变量,此时因为NSMutableDictionary的对象先init了一次,引用计数是1
,然后又retain了一次,引用计数+1,变为2,最后函数结束的时候又release了一次,引用计数-1,变为1了,
最后接着看汇编代码333行,调用了
_objc_autoreleaseReturnValue,先看这个方法的实现
image.png
看第三行如果prepareOptimizedReturn为true,直接返回该对象,否则加入自动释放池里面,待下一个runloop到来时释放,那么这个prepareOptimizedReturn是什么意思呢
看下prepareOptimizedReturn的实现
image.png
上面注释写的很清楚,尝试优化,否则返回的值必须retain,autorelease,尝试这个词是不是很熟悉,上面提到过,其实这个优化就是ARC在运行时的优化,就是调用_objc_autoreleaseReturnValue是会检查,接下来是否会调用_objc_retainAutoreleasedReturnValue,如果会,那么就直接返回对象,直接跳过autorelease和retain,如果不会,则会autorelease
网上的一个例子时候说的很清楚
当方法全部基于 ARC 实现时,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue() 以替代 MRC 下的 autorelease。在 MRC 下需要 retain 的位置,ARC 会调用 objc_retainAutoreleasedReturnValue()。因此下面的 ARC 代码:
+ (instancetype)createSark {
return [self new];
}
// caller
Sark *sark = [Sark createSark];
实际上会被改写成类似这样:
+ (instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release
有了这个基础,ARC 可以使用一些优化技术。在调用 objc_autoreleaseReturnValue() 时,会在栈上查询 return address 以确定 return value 是否会被直接传给 objc_retainAutoreleasedReturnValue()。 如果没传,说明返回值不能直接从提供方发送给接收方,这时就会调用 autorelease。反之,如果返回值能顺利的从提供方传送给接收方,那么就会直接跳过 autorelease 过程,并且修改 return address 以跳过 objc_retainAutoreleasedReturnValue()过程,这样就跳过了整个 autorelease 和 retain的过程。
核心思想:当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要做就好了。
另外,当函数的调用方是非 ARC 环境时,ARC 还会进行更多的判断,在这里不再详述,详见 《黑幕背后的 Autorelease》
对应的objc_retainAutoreleasedReturnValue方法也是一样
1.id objc_retainAutoreleasedReturnValue(id obj)
2. {
3. if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
4 . return objc_retain(obj);
5.}
这里第3行代码也会判断是否优化,如果优化就直接返回对象,没有必要在retain了,多余
跟objc_autoreleaseReturnValue对应,objc_autoreleaseReturnValue判断如果可以优化,则把标志位存到Threadlocal里面,objc_retainAutoreleasedReturnValue则调用acceptOptimizedReturn从ThreadLocal里面取出来
所以到[arc testReturnDic]方法结束,NSMutableDictionary对象的引用计数依然是1
,在继续看
再来看看ViewControll中调用[arc testReturnDic]的汇编语言
image.png
可以看出第483行确实调用了_objc_retainAutoreleasedReturnValue,上面说过了,如果是优化就直接返回返回,不retain,所以引用计数依然是1,至此[arc testReturnDic]的分析结束
再来看看[arc copyTestARCDic]的分析,同样,先看汇编代码
image.png
跟[arc testReturnDic]很像,只是这里缺少_objc_autoreleaseReturnValue的调用,那么这里就有疑问了,那缺少_objc_autoreleaseReturnValue说明,这里不存在优化,那最终在ViewController里面_objc_retainAutoreleasedReturnValue,引用计数岂不是变为2了,其实不然,如果引用计数是2的话,函数结束只会释放一次局部变量,那依然存在1个引用计数啊,岂不是内存泄漏把,醒醒吧,IOS怎么可能出现这种低级错误,那么怎么解释这个问题,那就继续看汇编了
ViewControll中调用[arc copyTestARCDic]的汇编语言
image.png
答案揭晓了,这里面并没有调用_objc_retainAutoreleasedReturnValue,为什么呢?其实上面分析MRC的时候提过
这是因为根据苹果的命名规定,调用以alloc/new/copy/mutableCopy等开头的方法,表示调用者自己生成并持有对象,所以不需要retian,即在编译器识别这些方法时,不会自动加上
到此前4行日志分析完毕,接下来分析第5行日志
NSDictionary *adExtraDic5 = @{@"pull_time": @(1),
@"pull_time_1": @(2)
};
为什么它的引用计数是2
这个字典的赋值语句,实际上在编译的时候调用
[NSDictionary dictionaryWithObjects:forKeys:count:],这个通过debug或者汇编语言都可以知晓,然后接着会[__NSSingleEntryDictionaryI __new:::]一下,所以此时引用计数是1,
这个为什么不用汇编语言来分析呢?因为NSDictionary是系统类,无法看到汇编的代码
这是赋值语句的调用栈
image.png
下面再看看ViewControll中调用 NSDictionary *adExtraDic5 = @{@"pull_time": @(1),@"pull_time_1": @(2) }发生了什么
首先通过debug发现它会调用autorelease
调用栈如下
image.png
然后会调用objc_retainAutoreleasedReturnValue
调用栈如下
image.png
这样因为前面没有调用_objc_autoreleaseReturnValue,也就是说没有优化,所以这里的的objc_retainAutoreleasedReturnValue会retain对象,导致引用计数+1,所以此时引用计数为2
附上ViewControll中该段代码的汇编
image.png
里面也有_objc_autoreleaseReturnValue的调用,但是没有autorelease的调用,不明白什么原因
至此第5行的日志也分析完毕,
通过上面的分析基本对ARC和MRC有个大致的了解,所以接下来的日志分析也比较好理解了,
先看这个日志
image.png
这些个weak引用的是前面分析的那些对象,当这些对象释放内存时,weak就为nil,这就可以判断对象什么时候释放了,这行日志的打印的时候,很明显这些都没有被释放,所以都不为nil
再看看这个日志
image.png可以看到除了testWeak2和testWeak5之外,其他都为nil,这说明除了adExtraDic2和adExtraDic5其他对象都被释放了
分析之前先贴一张testReference函数结束时的汇编语言 image.png 136行对应的就是函数结束的位置
可以看出里面很多_objc_storeStrong,这就是对象释放局部变量的操作
下面来一个个分析,为什么被释放,为什么没被释放
testWeak1对象的是adExtraDic1,引用计数是1,当testReference结束时,函数出栈,释放一次局部变量,导致引用计数是0,释放内存,
思考个问题,如果TestMRC copyTest在返回是添加了autorelease即 [adExtraDic autorelease]
会出现什么情况.....,30秒过去了...
这是因为adExtraDic1的引用计数是1,它在testReference结束的时候,因为释放局部变量,导致,引用计数变为0了,内存已经释放,此时因为有autorelease,它会等到下一次loop的到来时,尝试再一次释放该对象,但是对象在已经释放完毕,所以会崩溃
再来看看testWeak2对应的adExtraDic2为啥在这个testweak方法中没被释放
跟adExtraDic1,当testReference结束时,函数出栈,释放一次局部变量,导致引用计数-1,但是因为adExtraDic2本身的引用计数为2,即使-1,剩下1,所以不会释放
testWeak3 testWeak4 testWeak5的情况都是类似,就不一一分析
这段日志实际上就是模拟下一次runloop的到来,会发生什么情况,这里的代码dispatch_after就是模拟下一个runloop
可以看到testWeak2和testWeak5都为nil了,其实就是testWeak2和testWeak5的autorelease的作用,前面也说过autorelease会延迟对象的释放,等下次runloop时才会释放,所以这里又释放了一次,
至此所有的日志分析结束
再来看看特殊的case吧
NSDictionary *adExtraDic5 = [[NSDictionary alloc]init];
这个adExtraDic5对象的引用计数是-1或者无穷大,不管怎么样都不会被释放,猜测是因为NSDictionary是不变的字典,这样的创建实际上没有任何意义
NSString *ff = @"dsdsddwdasdsdaddasssa";
这个adExtraDic5对象的引用计数是-1或者无穷大,这个字符串位于常量区,不是堆区,没有引用计数,不存在释放
NSString *str = [NSString stringWithFormat:@"123456789"];
NSString *longStr = [NSString stringWithFormat:@"1234567890"];
NSLog(@"str %s %p", object_getClassName(str), str);
NSLog(@"longStr %s %p", object_getClassName(longStr), longStr);
str沒有引用计数,longStr的引用计数是1
为什么呢
在网上搜索了一下,一般人给出的答案是:当字符串长度小于10时,字符串是保存在常量区,没有引用计数。如果长度大于等于10呢,就会被复制到堆去,有引用计数。
参考链接
Tagged Pointer 具体了解一下
最后在扩展一些小知识
关于ARC和MRC属性赋值的问题,
大家知道ios中属性的赋值,实际上是调用set方法
比如说有个TestARC.h文件
@interface TestARC : NSObject
@property (nonatomic,retain)NSObject * obj;
调用testArc.obj =[[ NSObject alloc]init]
实际上是调用[testArc setObj]方法
objc_storeStrong的实现
image.png 可以看出,对于ARC的nonatomic属性来说,先把以前的该属性的对象prev取出来,然后赋予新值,最后释放对象,那如果多个线程同时赋值,就会有同步问题了。比如 image.png 这段代码在nonatomic属性下必crash,崩溃日志 image.png 这个很好理解,多线程导致同一个对象被释放了多次下面再看看如果是atomic属性呢》它调用的是objc_setProperty_atomic->reallySetProperty,看看reallySetProperty的实现 image.png
第89行判断如果是非原子性,直接赋值,不加锁,否则
枷锁,这样就解决了的线程安全问题,所以上面的例子如果是atomic就没问题
再来看看
总结:没啥总结的,多看汇编,多看源码
参考链接
《黑幕背后的 Autorelease》
NSString 引用计数
Tagged Pointer 具体了解一下