iOS中的Reference Counting詳解

霧花_小路發表於2018-05-05

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訊息的說明

  • 在API文件中,蘋果直戳了當的表示:

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開發人員,個人認為還是有必要了解一些關於引用計數的知識的,這也是知乎出這道題的意義。

相關文章