0x00 問題的引入
- 前一陣子表哥給了我一道知乎的iOS開發崗位面試題,據說仍是那種相似於「一票否決」的題目,考察應試者的編程能力。我仔細一看是關於MRC的一道題,也就是在考察Reference Counting。(代碼爲了方便運行測試,略有改動,可是核心思路無變化)
// 應用背景:MRC模式
// 請說出全部NSLog的輸出值,並解釋理由。
#import <Foundation/Foundation.h>
@interface Zhihu : NSObject
+ (int) setKanShanToZhihu;
@end
@implementation Zhihu
+ (int) setKanShanToZhihu {
NSMutableArray *zhihu=[[[NSMutableArray alloc]init]retain];
NSObject *kanshan=[[NSObject alloc]init];
[kanshan retain];
[zhihu addObject:kanshan];
NSLog(@"%d",(int)[kanshan retainCount]);
[kanshan retain];
[kanshan release];
[kanshan release];
NSLog(@"%d",(int)[kanshan retainCount]);
[zhihu removeAllObjects];
NSLog(@"%d",(int)[kanshan retainCount]);
[kanshan release];
return (int)[kanshan retainCount]+(int)[zhihu retainCount];
}
@end
int main(int argc, const char * argv[]) {
NSLog(@"%d",[Zhihu setKanShanToZhihu]);
return 0;
}
- 你們能夠去編譯一下這個題目,新建一個Xcode工程,在Compile Source中加入
-fno-objc-arc
,關閉ARC,運行結果是3 2 1 3
。
0x01 MRC和ARC
- 最先的時候Objective-C和C++同樣,也是手動管理內存的。不過OC使用的是Reference Counting(引用計數)的方式,也就是說,同一個內存空間,引用計數顯示了目前有多少個指針正在指向這個內存空間。顯然,當引用計數等於0的時候,這塊內存就再也不有用了,系統就會將其空間釋放。這裏面OC就提供了一些方法,容許程序員管理引用計數。
//對該對象的引用計數+1,返回一個新的指針指向該內存
- (instancetype) retain;
//對該對象的引用計數-1
- (oneway void) release;
//輸出該對象的引用計數數值
- (NSUInteger) retainCount;
- 你們也看出來了,這樣管理也很麻煩,程序員須要關注大量的指針問題,還有可能出現強引用循環的問題。因此從iOS 5.0開始,蘋果引入了ARC機制,ARC即自動引用計數(Automatic Reference Counting),這才把iOS開發者們從引用計數中解放出來,而原來的方式就稱爲MRC了,即手動引用計數(Manual Reference Counting)。
- 既然iOS 5以後就能夠用ARC了,如今App Store的最低支持版本已是iOS 8.0了,爲何知乎的面試題還要考MRC呢?顯然是爲了考察iOS面試者對於引用計數機制的瞭解啦。
0x02 題目解答
- 知乎這個面試題確實很考驗iOS面試者的編程功底,將手動引用計數的管理應用到了極致。
- 首先第1句建立了一個可變長度的數組,變量名字爲zhihu
NSMutableArray *zhihu=[[[NSMutableArray alloc]init]retain];
- 這裏咱們發送
alloc
消息,就會分配一塊內存,再發送init
消息進行初始化,這樣自己就會返回一個指針,引用計數變爲1。可是恰恰又調用了一次retain
,這時候引用計數又會加1,變爲2。因此這一個語句使得引用計數+2。
- 第2句和第3句正常建立了一個對象kanshan,引用計數爲1,隨後又進行了
retain
,引用計數爲2。
NSObject *kanshan=[[NSObject alloc]init];
[kanshan retain];
- 第4句經過向zhihu數組發送
addObject
消息,將kanshan對象加入數組。
[zhihu addObject:kanshan];
- 這裏由於向數組發送了
addObject
消息,數組中就也會保存一個指針指向這片內存,kanshan的引用計數再次加1。因此第一次輸出的結果爲3
。
- 而後連續通過三句話,引用計數先加1後減2,結果固然是減1。
[kanshan retain];
[kanshan release];
[kanshan release];
- 因此第二次輸出的結果爲
2
。
- 隨後又向zhihu發送了
removeAllObjects
消息,清空了整個數組
[zhihu removeAllObjects];
- 這時候kanshan的引用計數也會受到影響,由於它再也不保存在數組中,因此引用計數減1,第三次輸出的結果是
1
。
- 最後又調用了一次
release
[kanshan release];
return (int)[kanshan retainCount]+(int)[zhihu retainCount];
- 按理來講kanshan的引用計數應該降爲0了,被釋放。可是在發送
retainCount
消息的時候,爲了不對已經釋放的內存發送消息,系統會自動持有一個指向該塊內存的指針。而zhihu的引用計數仍是爲2,因此,最後返回值的總計數值是3
。
0x03 API文檔中對retainCount消息的說明
Do not use this method.
This method is of no value in debugging memory management issues. Because any number of framework objects may have retained an object in order to hold references to it, while at the same time autorelease pools may be holding any number of deferred releases on an object, it is very unlikely that you can get useful information from this method.
- 這個函數對調試內存管理問題沒有用處,由於全部的在framework中的對象都會保留了一個對象以便於持有對這個對象的引用。同時在autorelease pool中對象也可能被延遲釋放。因此說咱們不可能從這裏獲取有關內存管理的有用信息。
- 因此,即便咱們把kanshan的引用計數降爲0,系統仍然會保留一個指針指向kanshan,以持有一個對kanshan的引用,這樣咱們在調用retainCount的時候纔不會出現錯誤。
0x04 總結
- 實際上蘋果費盡心思讓咱們不要使用MRC,不要去考慮引用計數的問題。爲了解決強引用循環的問題,蘋果甚至設計了weak指針。在最新的Swift語言中,也必須進行一些unsafe的生命才容許你手動管理內存。可是做爲一名合格的iOS開發人員,我的認爲仍是有必要了解一些關於引用計數的知識的,這也是知乎出這道題的意義。