【持续更新】这些iOS冷知识,你知道吗?

这里是我在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

果然如此。dicRefdicRef1的地址是一样的,而且第二次打印时,在没有对dicRef对象执行任何操作的情况下,它的引用计数居然又加了1。

那么我们可以大胆猜测:

实际上,每次调用CFNetworkCopySystemProxySettings()返回的地址一直是同一个,未调用时它的引用计数就为1,而且每调用一次,引用计数都会加1

如此看来,CFNetworkCopySystemProxySettings()报道查看的对象在引用计数上的表现状语从句:其它系统-单例十分形容词:,比如[UIApplication sharedApplication][UIPasteboard generalPasteboard][NSNotificationCenter defaultCenter]等。

单例对象重新建立,对象指针会保存在静态区,单例对象在堆中分配的内存空间,只在应用程序终止后才会被释放。

【持续更新】这些iOS冷知识,你知道吗?_第1张图片

 

因此对于某种单一实例对象,调用一次就需要释放一次(ARC下OC对象无需手动释放),保持其引用计数为1(而不是0),保证其不被系统回收,下次调用时,依然能正常访问。

块属性用什么​​修饰

问题背景

这个问题逐步一道司空见惯的面试题:

iOS种block属性用什么​​修饰?(copy还是strong?)

堆栈溢出上也有相关的问题:可可块成为强大的指针vs复制。

问题探讨

先来回顾一些概念。

iOS内存分区为:栈区,堆区,分区区,常量区,代码区(地址从高到低)。常见的块有以下几种:

  • NSGlobalBlock:存在区域区的块;
  • NSStackBlock:存在栈区的块;
  • NSMallocBlock:存在堆区的块。

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环境下,用copystrong都可以。

看似没什么问题,于是我在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时期,只有assignretaincopy修饰符,strongweak是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查看汇编代码,点击进入步骤。

【持续更新】这些iOS冷知识,你知道吗?_第2张图片

callq指令中可以看到声明的copyBlock属性具有copy的特性。

然后断点打在self.retainBlock的声明函数这一行,再进入查看,可以注意到keepBlock不具有copy的特性。

【持续更新】这些iOS冷知识,你知道吗?_第3张图片

再在ARC下试一试。把断点打在self.strongBlock的声明函数这一行,进入查看,可以发现,用strong修饰的属性,也具有copy的特性。

【持续更新】这些iOS冷知识,你知道吗?_第4张图片

 

这也就很好解释了为什么MRC下用retain修饰的属性位于栈区,而用copystrong修饰的属性存在堆区。

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的内存,它被转移到了堆区。


暂时先总结到这里,后续如果有新的发现,我也会在此文中继续补充,欢迎订阅,收藏〜

你可能感兴趣的:(【持续更新】这些iOS冷知识,你知道吗?)