Objective-C 內存管理基礎

前言

之前的兩篇拙文C語言-內存管理基礎C語言-內存管理深入 介紹了關于C語言在內存管理方面的相關知識。但是對于從事iOS開發的同胞們來說,顯然Objective-C用的更多,所以筆者想用兩篇文章盡量完整的介紹一下Objective-C的內存管理,本文為第一部分,將從類和對象所有權策略及引用計數機制內存管理原則內存管理方式等幾個方面展開。如果你能賞臉閱讀此文,你會發現本文利用近一半的篇幅介紹ObjC對象的相關知識,這是因為Objective-C內存管理-管理的是繼承自NSObject的對象的內存。閱讀本文要求讀者對C語言內存管理有一定的了解,尚不熟悉的同學請移步到這里:C語言-內存管理基礎。話不多說,先附上本文內容的思維導圖。

主要內容思維導圖

類的結構與加載過程

Objective-C作為一門擴充C的面向對象編程語言,其和C語言的區別之一在于引入“面向對象”思想,能夠靈活的使用類和對象進行編程。因此了解類的結構和本質對于學習Objective-C是非常重要的。

  • 類的結構
    Objective-C中的類是本身也是一個Class類型的對象,簡稱類對象。而Class實際上是一個指向objc_class結構體的指針。
typedef struct objc_class *Class;

通過查看 objc/runtime.h文件,得知objc_class結構組成,其中包括了一個類很多信息。

struct objc_class {
  Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
  Class super_class                       OBJC2_UNAVAILABLE;  // 父類
  const char *name                        OBJC2_UNAVAILABLE;  // 類名
  long version                            OBJC2_UNAVAILABLE;  // 類的版本信息,默認為0
  long info                               OBJC2_UNAVAILABLE;  // 類信息,供運行期使用的一些位標識
  long instance_size                      OBJC2_UNAVAILABLE;  // 該類的實例變量大小
  struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 該類的成員變量鏈表
  struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定義的鏈表
  struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法緩存
  struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 協議鏈表
#endif
} OBJC2_UNAVAILABLE;
ObjC對象結構圖
  • 獲取內存中的類對象
  • 利用類的class方法
    【函數原型】+ (Class)class
    【方法說明】類方法,通過特定的一個類調用該方法返回一個Class類型的類對象。
Class classP = [Person class]; //classP現在一個`Class `類型的類對象
  • 利用類的實例對象的class方法
    【函數原型】- (Class)class
    【方法說明】對象方法,通過某個類的實例對象調用該方法返回一個Class類型的類對象。
Person *objectP = [[Person alloc] init];
Class classP1 = [objectP class];

可以利用objectP類對象創建Person類的實例對象,并通過打印確定classPPerson類型。

Person *objectP1 = [[classP new] init];
NSLog(@"%@", NSStringFromClass(classP)); 

控制臺輸出:

//2017-02-05 15:49:57.916948 類和對象[4473:131075] Person

打印classPclassP 1內存地址發現數值相同,說明類在內存中只有一份。

NSLog(@"%p, %p",classP, classP1);
// 2017-02-05 15:28:53.340127 類和對象[4238:123830] 0x1000011e0, 0x1000011e0
  • 類的加載和初始化
  • 類的加載
    【函數原型】+ (void)load
    【函數說明】 程序運行把Xcode的"Compile Sources"選項中存在的類和分類(不管有沒有用到)加載進來時調用,先調用父類再調用子類,每個類調用一次。
  • 類的初始化
    【函數原型】+ (void)initialize
    【函數說明】第一次使用到該類時調用。優先調用該類分類中的initialize方法,子類調用時會先調用父類的initialize方法初始化父類。

【區別】:load方法類和分類都會調用,因為他們是分開加載的,分類的加載順序和編譯順序有關;initialize方法是首次使用到該類時調用,調用順序是先調用該類分類中initialize,再調用該類父類分類的initialize;如果沒有會調用該類的initialize,再調用父類的initialize


對象的創建

Objective-C中創建對象,先調用類的alloc方法返回對象再調用對象的initinitWithSomething方法返回自己亦或是直接調用該類的new方法。例如下面代碼:

  Person *p1 = [[Person alloc] init];
  Person *p2 = [Person new];

依次介紹下面三個方法:

  • + (instancetype) alloc
    該方法被調用時系統首先在堆區分配合適大小的內存存儲該對象(參考C語言-內存管理基礎-C語言操作堆內存的函數部分內容),并返回一個未被初始化的該類型的對象,并完成下面三件事情:(引自談ObjC兩段構造模式

1,將該新對象的引用計數 (Retain Count) 設置成 1。
2,將該新對象的 isa 成員變量指向它的類對象。
3,將該新對象的所有其它成員變量的值設置成零。(根據成員變量類型,零有可能是指 nil 或 Nil 或 0.0)

  • - (instancetype) init
    根據對象具體成員變量的類型真正初始化對象的成員變量的值。

  • + (instancetype) new
    可以簡單的理解為將 allocinit合并為一次操作。

  • 對象的存儲
    上述創建p1p2的代碼可以解讀為:
  • 系統在內存的棧區分別開辟空間存儲p1p2指針變量
  • 系統在內存的堆區分別開辟空間存儲兩個Person對象并使棧區的p1p2指針變量分別指向堆區的Person對象。
    對象存儲簡單圖解

對象的所有權及引用計數

  • 所有權策略:任何自己創建的對象都歸自己所有且都存在一個或多個所有者,只要對象至少存在一個所有者,該對象就不會被銷毀,其占用的內存空間就會一直存在不被釋放(除非整個程序已經退出)。類似于一瓶礦泉水可能被一個或多個人飲用,只要還有一個人需要飲用該礦泉水,它就不會被環衛工人回收。
    通過NSObject協議中的retainCount屬性可以獲得該對象當前的引用計數值。

  • 引用計數器:Cocoa采用一種引用計數機制,為每個對象都綁定一個NSUInteger類型的整數表示該對象當前被引用的次數(即當前共有多少個所有者引用著該對象),稱之為該對象的引用計數器。每個ObjC對象都有自己的引用計數器(在64位編譯器環境下占用8個字節空間)。類似于一個整數記錄一瓶礦泉水當前被多少個人飲用,當該礦泉水沒有被人飲用時就要被環衛工人回收再利用了。

  • 引用計數器的作用:對象剛被創建時其引用計數默認為1,當對象的引用計數器值變為0時(即已經沒有所有者引用該對象),系統銷毀該對象,釋放并重新利用該對象在堆區對應分配的存儲空間。系統通過判斷對象的引用計數器值是否為0來決定是否需要銷毀該對象并釋放其占用的內存空間。
    一種例外情況:若對象值為nil時其引用計數為0但系統不回收空間,因為系統尚未為該對象分配空間。

Person *p1 = nil;
 NSLog(@"%lu", (unsigned long)p1.retainCount); //p1.retainCount = 0
  • 與引用計數相關的操作
  • - (instancetype)retain:使對象的引用計數器值+1
  • - (oneway void)release:使對象的引用計數器值-1(不代表銷毀該對象)
  • - (NSUInteger)retainCount:獲得對象當前的引用計數器值
  • - (instancetype)autorelease:待稍后清理“自動釋放池”(@autoreleasepool)時,再減少對象的引用計數

值得注意的是:retain操作無法使一個已經被釋放的對象的引用計數器值+1,這也很好理解,retain無法讓一個已經死去的人起死回生,就像環衛工人已經回收了你的礦泉水瓶,那么你將不能再持有該水瓶。

UIView *view = [[UIView alloc] init];
NSLog(@"創建之后的默認值是:%ld",(unsigned long)view.retainCount); //創建之后的默認值是:1
[view retain];
NSLog(@"通過一次retain操作之后:%ld",(unsigned long)view.retainCount); //通過一次retain操作之后:2
[view release];
NSLog(@"通過一次release操作之后:%ld",(unsigned long)view.retainCount); //通過一次release操作之后:1
上面的四種操作中除開`autorelease `方法外都比較好理解,關于`autorelease `可以點擊[黑幕背后的Autorelease](http://blog.sunnyxx.com/2014/10/15/behind-autorelease/)。同時《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》書中有這樣解釋的:

在OC引用計數框架中,自動釋放池是一項重要特性,調用release會立刻遞減對象的引用計數(而且很可能令系統回收此對象)然而有時候可以不調用它,改為調用autorelease,此方法會在稍后遞減計數,通常是在下一次“事件循環“(even loop)時遞減,不過也可能執行的更早。此特性很有用,尤其是在方法中返回對象時更應該用它。

該書中還舉出了這樣的例子:

- (NSString *)stringValue {
  NSString *str = [[NSString alloc] initWithFormat:@"i am this: %@",self];
  return str;
}

此時返回的str對象計數值比期望值要+1,因為方法內部調用了一次alloc操作而又沒有與之對應的釋放操作,計數器+1就意味著調用者要負責處理多出來的這一次保留操作。但是不能在- (NSString *)stringValue方法內部釋放,否則還沒等方法返回,系統就把該對象回收了,這時候應該使用autorelease,他能夠延長對象的生命周期,保證方法返回后該對象一定有效并且在合適的時機得以釋放。

【注意】:關于一個對象的retainCount大部分情況下是合理的,但是有些時候其數值是令人無法理解的。例如蘋果官方文檔對其做了下面的解釋:

蘋果官方對`retainCount`的解釋

文檔的大致內容是:此方法在調試內存管理問題上作用不大。 因為任意的框架對象可能引用該對象,同時自動釋放池對對象有延遲釋放作用,因此無法通過調用此方法獲取到有用的信息。

  • 對象的銷毀

    • 對象何時被銷毀:當對象的引用計數器值為0時,它將被系統銷毀,其占用的內存空間將得到釋放。
    • - (void)dealloc:當對象被銷毀時,系統會調用該方法,重寫該方法并在其中釋放相應的資源如移除通知等(類似于臨終遺言),一旦重寫dealloc方法就必須在代碼塊最后調用[super dealloc]方法,調用[super dealloc]方法是為了讓super釋放相應的資源,使得任何繼承來的對象都能夠得到釋放;另外我們不應該直接調用對象的delloc方法。

    例如下面的代碼:

@implementation Person
   - (void)dealloc
   {
    NSLog(@"對象被銷毀了");
    [super dealloc];
   }
@end
 Person *p1 = [[Person alloc] init];
 NSLog(@"%lu",(unsigned long)p1.retainCount);
 [p1 release];

控制臺輸出信息

2017-01-28 18:11:29.431764 OC中對象內存管理[20948:160747] 1
2017-01-28 18:11:29.432173 OC中對象內存管理[20948:160747] 對象被銷毀了

內存管理的范圍

有了上面關于Objc對象知識的鋪墊,我們可以很好的引入內存管理這個概念。

  • 范圍:管理任何繼承自NSObject類型的對象的內存。對于其他基本數據類型如:int、char、double、結構體、枚舉類型的變量無效。

  • 根本原因:ObjC對象和其他的基本類型變量在內存中存儲位置不同,對象所占的內存由系統在堆區動態分配需要程序員管理手動釋放,而其他基本數據類型的變量(局部變量)一般分配在棧區由系統自動釋放。

內存管理的方式

  • Objective-C中為我們提供了兩種(曾經是三種)管理內存的方式:
    • Mannul Reference Counting (MRC)
      手動內存管理,指的是通過retainreleaseautorelease等使用引用計數器操作的方式由程序員手動管理內存。本文中所有涉及到以上三個方法的代碼都是MRC的實踐。MRC最大的問題在于持有和釋放對象的時機,不當的retainrelease極可能產生“野指針”和“內存泄露”等內存方面的問題(下文中有具體介紹)。

    • Automatic Reference Counting (ARC)
      自動內存管理,ARC作為WWDC2011和iOS5之后LLVM 3.0編譯器的一項新特性,極大的解放了iOS開發者的雙手(蘋果推薦使用)。在ARC的編譯環境下開發者再也不用寫任何含有retainreleaseautorelease的代碼了,編譯器將自動在合適的地方插入上述代碼實現內存的管理。事實上現在絕大部分代碼都是使用的ARC管理內存,只是作為初學者可能“日用而不知”,并沒有意識到其中深層次的關系,并且在ARC環境下編寫含有引用計數操作的代碼是無法編譯通過的。例如:

      ARC與引用計數操作

      ARC與 [supper delloc]

MRC和ARC的區別示意圖

關于更多MAC和ARC機制的內容請參考以下博客:
王巍 (@onevcat):手把手教你ARC——iOS/Mac開發ARC入門和使用
HIT-Alibaba:Objective-C 中的內存分配
Apple:Transitioning to ARC Release Notes

  • Gargage Collection (垃圾回收機制)
    Objective-C 2.0以后存在垃圾回收機制。垃圾回收機制監視整個對象關系圖,查找那些在作用域內已沒有任何指針指向的對象,并自動釋放這些對象。
    另外在《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》一書中對垃圾回收機制有這樣的描述:

從Mac OS X10.8開始,“垃圾收集器”(garbage collector)已經正式廢棄了,以后Objective-C代碼編寫Mac OS X程序時不應再使用它,而iOS則從未支持過垃圾收集。

由于關于Objective-C垃圾回收的資料本人收集的比較少,僅在《好學的Objective-C》一書中看到過相關介紹,而且該機制從未在iOS開發中使用到,所以筆者這里姑且認為Objective-C中是存在過三種內存管理的方式,只是沒法介紹關于它的更多內容。有興趣的同學可以查閱一下該書。

內存管理的原則

  • 只要對象還在被使用其占用的內存空間就不應該被回收;需要使用該對象,使該對象引用計數+1;使用完該對象,使該對象的引用計數-1。
  • 誰創建,誰release
  • 誰retain,誰release

具體來說當使用allocnewcopy(生成一個接受對象的副本)創建對象時,其引用計數器值被置為1,當不需要使用該對象時需要做一次release操作;當使用retain操作持有一個對象時其引用計數器值+1,當不需要使用該對象時需要做一次release操作;一次創建對應著一次release,一次retain對應著一次release,這樣對象才能“有始有終”。

內存管理不當導致的問題

  • 野指針
  • 定義的指針變量沒有初始化。
  • 指向的堆內存空間已經被釋放的指針。
Dog *yellowDog = [[Dog alloc] init];
[yellowDog release];
NSLog(@"%@", yellowDog);

上面的代碼在yellowDog 的引用計數器值為0時已經將在堆區分配的 Dog 對象的內存釋放掉了,再調用NSLog通過yellowDog指針訪問堆區的對象就很可能會出問題。

Person *p1 = [Person new];
[p1 release];
[p1 release];

如上的代碼,當第一次執行[p1 release]代碼時,p1指向的對象的retainCount減為0,系統銷毀該對象且其在堆區的存儲空間會被回收,如果此時再次執行[p1 release]代碼,嘗試給已經被釋放的Pweson對象發送release消息,很可能會觸發運行時錯誤。具體來說程序運行成功但是控制臺會輸出這樣的信息:

Person object 0x1002049e0 overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug

《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》一書是這樣解釋 “很可能”這個詞的:

之所以說“很可能”沒有說一定,是因為對象在堆區所占的內存在“解除分配”(deallocated)之后只是放回到“可用內存池”,如果此時執行NSLog時尚未覆蓋對象內存,那么該對象依舊有效,這時程序不會奔潰。由此可見,因過早釋放對象而導致的bug很難調試。

當我們開啟Xcode的僵尸對象檢測功能時,程序運行崩潰,在控制臺輸出如下信息:

*** -[Person release]: message sent to deallocated instance 0x100200640
//release 消息發送給了一個已經釋放掉的對象

上面的情況在日常開發中常常體現為Thread:EXC_BAD_ACCESS(壞訪問錯誤)
【壞訪問錯誤】:即訪問了一塊壞內存(不可用的已經被回收的內存)。這樣的錯誤我們也稱為“野指針錯誤”,意思是利用野指針操作了一塊不可用的內存。
【僵尸對象】:所占用的內存空間已經被回收不可用的對象。僵尸對象不應該再使用。
【解決方案】:為避免在不經意間使用了無效對象,一般調完realease之后都會清空指針。這樣就能保證不會可能出現指向無效對象的指針。即在對象釋放完畢后將指向對象的指針置為nil,給nil發消息不會產生任何反應。
綜上所述:沒有置為nil卻指向了無效對象的指針就是野指針

所以正確的做法應該是:
Dog * yellowDog = [[Dog alloc] init];
[yellowDog release];
yellowDog = nil //對象釋放后,將指向對象的指針置為nil
  • 內存泄漏
{   //代碼塊內創建Dog對象
      Dog *husky = [[Dog alloc] init];
}

形如上面的代碼,當我們在創建Dog對象時系統分別在棧區為husky變量分配內存、在堆區為husky所指向的Dog對象分配內存,由于變量husky的作用域在大括號內屬于局部變量,當代碼塊執行完畢,棧中的husky變量就被釋放了,但是此時在代碼塊中并沒有對husky變量指向的堆區的對象存儲空間進行釋放,所以我們說堆區中對應的存儲空間就被泄露了。

總結為:基本的數據類型由于占用的存儲空間是固定的,一般存在棧區中。由于棧中主要存放的是局部變量,局部變量占用的內存空間是其所在的代碼塊或者是函數結束的時候自動回收,指向對象的指針也會被回收,這個過程不需要程序員管理。但是對象創建完成后是存放在堆區的,由于此時指向對象的指針已經被回收,但是對象仍然存在內存中,就會造成內存泄漏(申請的空間已經不再使用卻沒有被及時合理的釋放掉)。

文章最后

以上就是筆者對于Objective-C內存管理基礎認識的全部內容,部分描述引自書籍和其它博客文中已經做出了說明,但凡是本人認為重要的概念均附上其它博客的鏈接用于擴展相關知識。

另外:本文涉及到MRC的代碼需要在Xcode進行如下設置才能編譯通過:在Build Settings中,找到"Objective-C Automatic Reference Counting"這個選項,將它的值改為"NO"。

如果文中有任何紕漏或錯誤歡迎在評論區留言指出,本人將在第一時間修改過來;喜歡我的文章,可以關注我以此促進交流學習; 如果覺得此文戳中了你的G點請隨手點贊;轉載請注明出處,謝謝支持。

下一篇:Objective-C 內存管理深入

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容

  • 1.1 什么是自動引用計數 概念:在 LLVM 編譯器中設置 ARC(Automaitc Reference Co...
    __silhouette閱讀 5,213評論 1 17
  • 說起內存管理,看似老生常談,但是真正掌握內存管理并不簡單。小伙伴們要加油了。 Objective-C的內存管理機制...
    ARU閱讀 335評論 1 1
  • 29.理解引用計數 Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數...
    Code_Ninja閱讀 1,513評論 1 3
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,745評論 0 9
  • 內存管理 簡述OC中內存管理機制。與retain配對使用的方法是dealloc還是release,為什么?需要與a...
    丶逐漸閱讀 1,980評論 1 16