前言
之前的兩篇拙文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;
- 獲取內存中的類對象
- 利用類的
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
類的實例對象,并通過打印確定classP
是Person
類型。
Person *objectP1 = [[classP new] init];
NSLog(@"%@", NSStringFromClass(classP));
控制臺輸出:
//2017-02-05 15:49:57.916948 類和對象[4473:131075] Person
打印classP
、classP 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
方法返回對象再調用對象的init
或initWithSomething
方法返回自己亦或是直接調用該類的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
可以簡單的理解為將alloc
、init
合并為一次操作。
-
對象的存儲
上述創建p1
、p2
的代碼可以解讀為:
- 系統在內存的棧區分別開辟空間存儲
p1
、p2
指針變量 - 系統在內存的堆區分別開辟空間存儲兩個
Person
對象并使棧區的p1
、p2
指針變量分別指向堆區的Person
對象。
對象存儲簡單圖解
-
更多細節
關于ObjC對象的更多細節請參考下面的文章:
唐巧:談ObjC兩段構造模式
冰霜:Objc 對象的今生今世
Matt Gallagher:What is a meta-class in Objective-C?
上文中有部分描述引自前者,已經做出了說明。
對象的所有權及引用計數
所有權策略:任何自己創建的對象都歸自己所有且都存在一個或多個所有者,只要對象至少存在一個所有者,該對象就不會被銷毀,其占用的內存空間就會一直存在不被釋放(除非整個程序已經退出)。類似于一瓶礦泉水可能被一個或多個人飲用,只要還有一個人需要飲用該礦泉水,它就不會被環衛工人回收。
通過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
大部分情況下是合理的,但是有些時候其數值是令人無法理解的。例如蘋果官方文檔對其做了下面的解釋:
文檔的大致內容是:此方法在調試內存管理問題上作用不大。 因為任意的框架對象可能引用該對象,同時自動釋放池對對象有延遲釋放作用,因此無法通過調用此方法獲取到有用的信息。
-
對象的銷毀
- 對象何時被銷毀:當對象的引用計數器值為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)
手動內存管理,指的是通過retain
、release
、autorelease
等使用引用計數器操作的方式由程序員手動管理內存。本文中所有涉及到以上三個方法的代碼都是MRC的實踐。MRC最大的問題在于持有和釋放對象的時機,不當的retain
和release
極可能產生“野指針”和“內存泄露”等內存方面的問題(下文中有具體介紹)。-
Automatic Reference Counting (ARC)
自動內存管理,ARC作為WWDC2011和iOS5之后LLVM 3.0編譯器的一項新特性,極大的解放了iOS開發者的雙手(蘋果推薦使用)。在ARC的編譯環境下開發者再也不用寫任何含有retain
、release
、autorelease
的代碼了,編譯器將自動在合適的地方插入上述代碼實現內存的管理。事實上現在絕大部分代碼都是使用的ARC管理內存,只是作為初學者可能“日用而不知”,并沒有意識到其中深層次的關系,并且在ARC環境下編寫含有引用計數操作的代碼是無法編譯通過的。例如:
ARC與引用計數操作
ARC與 [supper delloc]
關于更多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
具體來說當使用alloc
、new
、copy
(生成一個接受對象的副本)創建對象時,其引用計數器值被置為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點請隨手點贊;轉載請注明出處,謝謝支持。