這些 iOS開發 冷知識,你知道嗎?

筆者最近在準備面試時候,回顧了一些過去寫的項目和知識點,從底層和原理的角度重新去看代碼和問題,發現了幾個有意思的地方。

單例對象的內存管理

問題背景

在解決 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,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!

推薦閱讀

iOS開發——最新 BAT面試題合集(持續更新中)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章