笔者最近在准备面试时候,回顾了一些过去写的项目和知识点,从底层和原理的角度重新去看代码和问题,发现了几个有意思的地方。
单例对象的内存管理
问题背景
在解决 App 防止抓包问题的时候,有一种常见的解决方案就是:检测是否存在代理服务器。其实现为:
+ (BOOL)getProxyStatus { CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings(); const CFStringRef proxyCFstr = CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPProxy); CFRelease(dicRef); NSString *proxy = (__bridge NSString*)(proxyCFstr); if(proxy) { return YES; } return NO;}
在我前面的一篇文章《iOS 内存泄漏场景与解决方案》中,有提到非 OC 对象在使用完毕后,需要我们手动释放。
那么上面这段代码中,在执行 CFRelease(dicRef); 之后,dicRef 是不是应该就被释放了呢?
问题探讨
让我们来写一段测试代码试试看:
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();NSLog(@"%ld, %p", CFGetRetainCount(dicRef), dicRef);CFRelease(dicRef);NSLog(@"%ld, %p", CFGetRetainCount(dicRef), dicRef);CFRelease(dicRef);NSLog(@"%ld, %p", CFGetRetainCount(dicRef), dicRef);
打印结果为:
2, 0x6000004b97201, 0x6000004b9720(lldb)
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
推荐阅读
iOS开发——最新 BAT面试题合集(持续更新中)
程序在运行到第三次 NSLog 的时候才崩溃,说明对 dicRef 对象 release 两次才能将他彻底释放。
这很奇怪,按照以往的经验,第一次打印 dicRef 的引用计数值不应该是 1 才对吗?
修改一下代码,继续测试:
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();CFRelease(dicRef);CFRelease(dicRef);NSLog(@"%p", CFNetworkCopySystemProxySettings());
这次运行到最后一行代码的时候,居然还是崩溃了。连 CFNetworkCopySystemProxySettings() 对象都直接从内存里被销毁了?难道 dicRef 没有重新创建对象,而是指向了真正的地址?
为了验证猜想,我们定义两份 dicRef 对象,并打印出他们的地址和引用计数。
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();NSLog(@"%p, %ld,", dicRef, CFGetRetainCount(dicRef));CFDictionaryRef dicRef1 = CFNetworkCopySystemProxySettings();NSLog(@"%p, %p, %ld, %ld", dicRef, dicRef1, CFGetRetainCount(dicRef), CFGetRetainCount(dicRef1));
打印结果为:
0x600003bd2040, 2,0x600003bd2040, 0x600003bd2040, 3, 3
果然如此。dicRef 和 dicRef1 的地址是一样的,而且第二次打印时,在没有对 dicRef 对象执行任何操作的情况下,它的引用计数居然又加了 1。
那么我们可以大胆猜测:
实际上,每次调用 CFNetworkCopySystemProxySettings() 返回的地址一直是同一个,未调用时它的引用计数就为 1,而且每调用一次,引用计数都会加 1。
如此看来,CFNetworkCopySystemProxySettings() 返回的对象在引用计数上的表现和其它系统单例十分相似,比如 [UIApplication sharedApplication]、[UIPasteboard generalPasteboard]、[NSNotificationCenter defaultCenter] 等。
单例对象一旦建立,对象指针会保存在静态区,单例对象在堆中分配的内存空间,只在应用程序终止后才会被释放。
[图片上传失败...(image-bc0a30-1593333632639)]
因此对于这类单例对象,调用一次就需要释放一次(ARC 下 OC 对象无需手动释放),保持它的引用计数为 1(而不是 0),保证其不被系统回收,下次调用时,依然能正常访问。
block 属性用什么修饰
问题背景
这个问题来源于一道司空见惯的面试题:
iOS 种 block 属性用什么修饰?(copy 还是 strong?)
Stack Overflow 上也有相关的问题:Cocoa blocks as strong pointers vs copy。
问题探讨
先来回顾一些概念。
iOS 内存分区为:栈区、堆区、全局区、常量区、代码区(地址从高到低)。常见的 block 有三种:
• NSGlobalBlock:存在全局区的 block;
• NSStackBlock:存在栈区的 block;
• NSMallocBlock:存在堆区的 block。
block 有自动捕获变量的特性。当 block 内部没有引入外部变量的时候,不管它用什么类型修饰,block 都会存在全局区,但如果引入了外部变量呢?
这个问题要在 ARC 和 MRC 两种环境下讨论。
Xcode 中设置 MRC 的开关:
1、全局设置:TARGETS → Build Settings → Apple Clang - Language - Objective-C → Objective-C Automatic Reference Counting 设为 No;(ARC 对应的是 Yes)
2、局部设置:TARGETS → Build Phases → Compile Sources → 找到需要设置的文件 → 在对应的 Compiler Flags 中设置 -fno-objc-arc。(ARC 对应的是 -fobjc-arc)
针对这个问题,网上有一种答案:
MRC 环境下,只能用 copy 修饰。使用 copy 修饰,会将栈区的 block 拷贝到堆区,但 strong 不行;
ARC 环境下,用 copy 和 strong 都可以。
看似没什么问题,于是我在 MRC 环境执行了如下代码:
// 分别用 copy 和 strong 修饰 block 属性@property (nonatomic, copy) void (^copyBlock)(void);@property (nonatomic, strong) void (^strongBlock)(void);int x = 0; // 打印 normalBlock 所在的内存地址void(^normalBlock)(void) = ^{ NSLog(@"%d", x);};NSLog(@"normalBlock: %@", normalBlock);// 打印 copyBlock 所在的内存地址self.copyBlock = ^(void) { NSLog(@"%d", x);};NSLog(@"copyBlock: %@", self.copyBlock);// 打印 strongBlock 所在的内存地址self.strongBlock = ^(void) { NSLog(@"%d", x);};NSLog(@"strongBlock: %@", self.strongBlock);
打印结果为:
normalBlock: <__NSStackBlock__: 0x7ffeee29b138>copyBlock: <__NSMallocBlock__: 0x6000021ac360>strongBlock: <__NSMallocBlock__: 0x600002198240>
从 normalBlock 的位置,我们可以看出,默认是存在栈区的,但是很奇怪的是,为什么 strongBlock 位于堆区?难道 MRC 时期用 strong 修饰就是可以的?
其实不然,要知道 MRC 时期,只有 assign、retain 和 copy 修饰符,strong 和 weak 是 ARC 时期才引入的。
strong 在 MRC 中对应的是 retain,我们来看一下在 MRC 下用这两个属性修饰 block 的区别。
// MRC 下分别用 copy 和 retain 修饰 block 属性@property (nonatomic, copy) void (^copyBlock)(void);@property (nonatomic, retain) void (^retainBlock)(void);// 打印 copyBlock 所在的内存地址int x = 0;self.copyBlock = ^(void) { NSLog(@"%d", x);};NSLog(@"copyBlock: %@", self.copyBlock);// 打印 retainBlock 所在的内存地址self.retainBlock = ^(void) { NSLog(@"%d", x);};NSLog(@"retainBlock: %@", self.retainBlock);
打印结果为:
copyBlock: <__NSMallocBlock__: 0x6000038f96b0>retainBlock: <__NSStackBlock__: 0x7ffeed0a90e0>
我们可以看到用 copy 修饰的 block 存在堆区,而 retain 修饰的 block 存在栈区。
那么修饰符的作用在哪里,为什么会出现不同的结果,我们通过反汇编来探究一下。
把断点打在 self.copyBlock 的声明函数这一行(在上述引用代码的第7行,不是 block 内部)。然后开启 Debug → Debug Workflow → Always show Disassembly 查看汇编代码,点击 Step into。
[图片上传失败...(image-7402b9-1593333632639)]
在 callq 指令中可以看到声明的 copyBlock 属性具有 copy 的特性。
然后断点打在 self.retainBlock 的声明函数这一行,再进入查看,可以注意到 retainBlock 不具有copy 的特性。
[图片上传失败...(image-59735a-1593333632639)]
再在 ARC 下试一试。把断点打在 self.strongBlock 的声明函数这一行,进入查看,可以发现,用 strong 修饰的属性,也具有 copy 的特性。
[图片上传失败...(image-5c83c7-1593333632639)]
这也就很好解释了为什么 MRC 下用 retain 修饰的属性位于栈区,而用 copy、strong 修饰的属性存在堆区。
MRC 下,在定义 block 属性时,使用 copy 是为了把 block 从栈区拷贝到堆区,因为栈区中的变量出了作用域之后就会被销毁,无法在全局使用,而把栈区的属性拷贝到堆区中全局共享,就不会被销毁了。
ARC 下,不需要使用 copy 修饰,因为 ARC 下的 block 属性本来就在堆区。
那为什么开发者基本上都只用 copy 呢?
这是 MRC 的历史遗留问题,上面也说到了,strong 是 ARC 时期引入的,开发者早已习惯了用 copy 来修饰 block 罢了。
最后再补充一个小知识点。
// ARC 下定义 normalBlock 后再打印其所在的内存地址void(^normalBlock)(void) = ^{ NSLog(@"%d", x);};NSLog(@"normalBlock: %@", normalBlock);// 直接打印某个 block 的内存地址NSLog(@"block: %@", ^{ NSLog(@"%d", x);});
打印结果为:
normalBlock: <__NSMallocBlock__: 0x600001ebe670>block: <__NSStackBlock__: 0x7ffee8752110>
block 的实现是相同的,为什么一个在堆区,一个在栈区?
这个现象叫做运算符重载。定义 normalBlock 的时候 = 实际上执行了一次 copy,为了管理 normalBlock 的内存,它被转移到了堆区。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:413038000,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
网友评论