筆者最近在準備面試時候,回顧了一些過去寫的項目和知識點,從底層和原理的角度重新去看代碼和問題,發現了幾個有意思的地方。
單例對象的內存管理
問題背景
在解決 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,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!