这里是我在GitHub里超爱的一位博主小姐姐分享的内容,转到CSDN,希望大家喜欢她的话也可以去GitHub关注一波。
- 作者:
菲汀
- 连结:
https://blog.fiteen.top/2020/ios-trivia/
- 版权:
Attribution-NonCommercial-NoDerivatives 4.0 International(CC BY-NC-ND 4.0)
疫情期间比较严重,回顾了一些过去写的项目和知识点,从扭曲和原理的角度重新去看代码和问题,发现了几个有意思的地方。
在解决App防止抓包问题的时候,有一种常见的解决方案就是:检测是否存在代理服务器。其实现为:
1
2
3
4
5
6
7
8
9
10
|
+(BOOL)getProxyStatus { CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings(); const CFStringRef proxyCFstr = CFDictionaryGetValue(dicRef,(const void *)kCFNetworkProxiesHTTPProxy); CFRelease(dicRef); NSString * proxy =(__bridge NSString *)(proxyCFstr); if(proxy){ 返回YES; } 返回NO ; }
|
在我前面的文章文章《iOS内存泄漏场景与解决方案》中,有提到非OC对象在使用完毕后,需要我们手动释放。
那么上面这段代码中,在执行CFRelease(dicRef);
之后,dicRef
是不是应该就被释放了呢?
让我们来写一段测试代码试试看:
1
2
3
4
5
6
|
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);
|
打印结果为:
1
2
3
|
2,0x6000004b9720
1,0x6000004b9720
(LLDB)
|
程序在运行到第三次NSLog
的时候才崩溃,说明对dicRef
对象释放两次才能将他彻底释放。
这很奇怪,按照以往的经验,第一次打印dicRef
的引用计数值不应该是1才对吗?
修改一下代码,继续测试:
1
2
3
4
|
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
CFRelease(dicRef);
CFRelease(dicRef);
NSLog(@“%p”,CFNetworkCopySystemProxySettings());
|
这次运行到最后一行代码的时候,居然还是崩溃了。连CFNetworkCopySystemProxySettings()
对象都直接从内存里被销毁了?难道dicRef
没有重新创建对象,而是指向了真正的地址?
为了验证猜想,我们定义两份dicRef
对象,并打印出他们的地址和引用计数。
1
2
3
4
|
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
NSLog(@“%p,%ld,”,dicRef,CFGetRetainCount(dicRef));
CFDictionaryRef dicRef1 = CFNetworkCopySystemProxySettings();
NSLog(@“%p,%p,%ld,%ld”,dicRef,dicRef1,CFGetRetainCount(dicRef),CFGetRetainCount(dicRef1));
|
打印结果为:
1
2
|
0x600003bd2040,2,
0x600003bd2040,0x600003bd2040,3,3
|
果然如此。dicRef
和dicRef1
的地址是一样的,而且第二次打印时,在没有对dicRef
对象执行任何操作的情况下,它的引用计数居然又加了1。
那么我们可以大胆猜测:
实际上,每次调用CFNetworkCopySystemProxySettings()
返回的地址一直是同一个,未调用时它的引用计数就为1,而且每调用一次,引用计数都会加1。
如此看来,CFNetworkCopySystemProxySettings()
报道查看的对象在引用计数上的表现状语从句:其它系统-单例十分形容词:,比如[UIApplication sharedApplication]
,[UIPasteboard generalPasteboard]
,[NSNotificationCenter defaultCenter]
等。
单例对象重新建立,对象指针会保存在静态区,单例对象在堆中分配的内存空间,只在应用程序终止后才会被释放。
因此对于某种单一实例对象,调用一次就需要释放一次(ARC下OC对象无需手动释放),保持其引用计数为1(而不是0),保证其不被系统回收,下次调用时,依然能正常访问。
这个问题逐步一道司空见惯的面试题:
iOS种
block
属性用什么修饰?(copy
还是strong
?)
堆栈溢出上也有相关的问题:可可块成为强大的指针vs复制。
先来回顾一些概念。
iOS内存分区为:栈区,堆区,分区区,常量区,代码区(地址从高到低)。常见的块有以下几种:
block有自动捕获变量的特性。当block内部没有约会外部变量的时候,不管它用什么类型修饰,block都会存在分区区,但如果约会了外部变量呢?
这个问题要在ARC和MRC两种环境下讨论。
Xcode中设置MRC的开关:
- 全局设置:TARGETS→交通
Build Settings
→交通Apple Clang - Language - Objective-C
→交通Objective-C Automatic Reference Counting
设为No
;(ARC对应的是Yes
)- 局部设置:TARGETS→交通
Build Phases
→交通Compile Sources
→交通找到需要设置的文件→交通在对应的Compiler Flags
中设置-fno-objc-arc
。(ARC对应的是-fobjc-arc
)
针对这个问题,网上有一种答案:
copy
修饰。使用copy
修饰,替换栈区的block拷贝到堆区,但strong
不行;copy
和strong
都可以。看似没什么问题,于是我在MRC环境执行了如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//分别用复制和强修饰块属性
@属性(非原子,副本)空隙(^ copyBlock)(空隙);
@属性(非原子,强)空隙(^ strongBlock)(空隙);
int x = 0 ; //打印normalBlock所在的内存地址void(^ normalBlock)(void)= ^ { NSLog(@“%d”,x); }; NSLog(@“ normalBlock:%@”,normalBlock);
//打印copyBlock所在的内存地址
self .copyBlock = ^(void){ NSLog(@“%d”,x); }; NSLog(@“ copyBlock:%@”,自我 .copyBlock);
//打印strongBlock所在的内存地址
自 .strongBlock = ^(空隙){ 的NSLog(@ “%d”,X); }; NSLog(@“ strongBlock:%@”,自身为 .strongBlock);
|
打印结果为:
1
2
3
|
normalBlock:<__ NSStackBlock__:0x7ffeee29b138>
copyBlock:<__ NSMallocBlock__:
0x6000021ac360strongBlock:<__ NSMallocBlock__:0x600002198240>
|
从normalBlock的位置,我们可以抛光,替代是存在栈区的,但是很奇怪的是,为什么strongBlock
位于堆区?难道MRC时期用strong
修饰就是可以的?
其实不然,要知道MRC时期,只有assign
,retain
和copy
修饰符,strong
和weak
是ARC时期才约会的。
strong
在MRC中对应的是retain
,我们来看一下在MRC下用这两个属性修饰block的区别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// MRC下分别用拷贝保留状语从句块修饰属性
@属性(非原子,复制)空隙(^ copyBlock)(空隙);
@属性(非原子,保留)空隙(^ retainBlock)(空隙);
//打印copyBlock所在的内存地址
int x = 0 ;
自我 .copyBlock = ^(void){ NSLog(@“%d”,x); }; NSLog(@“ copyBlock:%@”,自我 .copyBlock);
//打印retainBlock所在的内存地址
自 .retainBlock = ^(空隙){ 的NSLog(@ “%d”,X); }; NSLog(@“ retainBlock:%@”,self .retainBlock);
|
打印结果为:
1
2
|
copyBlock:<__ NSMallocBlock__:0x6000038f96b0>
keepBlock:<__ NSStackBlock__:0x7ffeed0a90e0>
|
我们可以看到用copy
修饰的block存在堆区,而retain
修饰的block存在栈区。
那么修饰符的作用在哪里,为什么会出现不同的结果,我们通过反汇编来探究一下。
断点把打在self.copyBlock
的声明函数这一行(在上述引用代码的第7行,不是块内部)。开启然后Debug
→交通Debug Workflow
→交通Always show Disassembly
查看汇编代码,点击进入步骤。
在callq
指令中可以看到声明的copyBlock属性具有copy
的特性。
然后断点打在self.retainBlock
的声明函数这一行,再进入查看,可以注意到keepBlock不具有copy
的特性。
再在ARC下试一试。把断点打在self.strongBlock
的声明函数这一行,进入查看,可以发现,用strong
修饰的属性,也具有copy
的特性。
这也就很好解释了为什么MRC下用retain
修饰的属性位于栈区,而用copy
,strong
修饰的属性存在堆区。
MRC下,在定义块属性时,使用copy
是为了把块从栈区复制到堆区,栈区中的变量由系统管理其生命周期,因此它出了作用域之后就会被销毁,无法使用,而把栈区的属性副本到堆区后,堆区中的元素由程序员来管理,就可以达到共享的目的。
ARC下,不需要使用copy
修饰,因为ARC下的block属性本来就在堆区。
那为什么开发者基本上都只用
copy
呢?这是MRC的历史遗留问题,上面也说到了,
strong
是ARC时期引入的,者开发早已习惯了用copy
来修饰块罢了。
最后再补充一个小知识点。
1
2
3
4
5
6
7
8
9
10
|
// ARC下定义normalBlock后再打印其所在的内存地址
void(^ normalBlock)(void)= ^ { NSLog(@“%d”,x); }; NSLog(@“ normalBlock:%@”,normalBlock);
//直接打印某个块的内存地址
NSLog(@“ block:%@”,^ { NSLog(@“%d”,x); });
|
打印结果为:
1
2
|
normalBlock:<__ NSMallocBlock__:0x600001ebe670>
块:<__ NSStackBlock__:0x7ffee8752110>
|
block的实现是相同的,为什么一个在堆区,一个在栈区?
这个现象称为运算符重载。定义normalBlock的时候=
实际上执行了一次copy
,为了管理normalBlock
的内存,它被转移到了堆区。
暂时先总结到这里,后续如果有新的发现,我也会在此文中继续补充,欢迎订阅,收藏〜