在Objective-C的內存管理中,其實就是引用計數(reference count)
的管理。內存管理就是在程序需要時程序員分配一段內存空間,而當使用完之后將它釋放。如果程序員對內存資源使用不當,有時不僅會造成內存資源浪費,甚至會導致程序crach。我們將會從引用計數和內存管理規則等基本概念開始,然后講述有哪些內存管理方法,最后注意有哪些常見內存問題。
memory management from apple document
為了解釋引用計數,我們做一個類比:員工在辦公室使用燈的情景。
引用PRo Multithreading and Memory Management for iOS and OS X的圖
從上面員工在辦公室使用燈的例子,我們對比一下燈的動作與Objective-C對象的動作有什么相似之處:
燈的動作 | Objective-C對象的動作 |
---|---|
開燈 | 創建一個對象并獲取它的所有權(ownership) |
使用燈 | 獲取對象的所有權 |
不使用燈 | 放棄對象的所有權 |
關燈 | 釋放對象 |
因為我們是通過引用計數來管理燈,那么我們也可以通過引用計數來管理使用Objective-C對象。
引用Pro Multithreading and Memory Management for iOS and OS X的圖
而Objective-C對象的動作對應有哪些方法以及這些方法對引用計數有什么影響?
Objective-C對象的動作 | Objective-C對象的方法 |
---|---|
1. 創建一個對象并獲取它的所有權 | alloc/new/copy/mutableCopy (RC = 1) |
2. 獲取對象的所有權 | retain (RC + 1) |
3. 放棄對象的所有權 | release (RC – 1) |
4. 釋放對象 | dealloc (RC = 0 ,此時會調用該方法) |
當你alloc
一個對象objc,此時RC=1;在某個地方你又retain
這個對象objc,此時RC加1,也就是RC=2;由于調用alloc/retain
一次,對應需要調用release
一次來釋放對象objc,所以你需要release
對象objc兩次,此時RC=0;而當RC=0時,系統會自動調用dealloc
方法釋放對象。
在開發中,我們常常都會使用到局部變量,局部變量一個特點就是當它超過作用域時,就會自動釋放。而autorelease pool跟局部變量類似,當執行代碼超過autorelease pool塊時,所有放在autorelease pool的對象都會自動調用release
。它的工作原理如下:
NSAutoreleasePool
對象autorelease
方法NSAutoreleasePool
對象
引用Pro Multithreading and Memory Management for iOS and OS X的圖
iOS 5/OS X Lion前的(等下會介紹引入ARC的寫法)實例代碼如下:
1 2 3 4 5 6 7 8 9 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // put object into pool id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; /* 超過autorelease pool作用域范圍時,obj會自動調用release方法 */ |
由于放在autorelease pool的對象并不會馬上釋放,如果有大量圖片數據放在這里的話,將會導致內存不足。
1 | for (int i = 0; i |
iOS/OS X內存管理方法有兩種:手動引用計數(Manual Reference Counting)和自動引用計數(Automatic Reference Counting)。從OS X Lion和iOS 5開始,不再需要程序員手動調用retain
和release
方法來管理Objective-C對象的內存,而是引入一種新的內存管理機制Automatic Reference Counting(ARC),簡單來說,它讓編譯器來代替程序員來自動加入retain
和release
方法來持有和放棄對象的所有權。
在ARC內存管理機制中,id
和其他對象類型變量必須是以下四個ownership qualifiers其中一個來修飾:
所以在管理Objective-C對象內存的時候,你必須選擇其中一個,下面會用一些列子來逐個解釋它們的含義以及如何選擇它們。
如果我想創建一個字符串,使用完之后將它釋放調用,使用MRC管理內存的寫法應該是這樣:
1 2 3 4 5 | { NSString *text = @"Hello, world"; //@"Hello, world"對象的RC=1 NSLog(@"%@", text); [text release]; //@"Hello, world"對象的RC=0 } |
而如果是使用ARC方式的話,就text
對象無需調用release
方法,而是當text
變量超過作用域時,編譯器來自動加入[text release]
方法來釋放內存
1 2 3 4 5 6 7 | { NSString *text = @"Hello, world"; //@"Hello, world"對象的RC=1 NSLog(@"%@", text); } /* * 當text超過作用域時,@"Hello, world"對象會自動釋放,RC=0 */ |
而當你將text
賦值給其他變量anotherText
時,MRC需要retain
一下來持有所有權,當text
和anotherText
使用完之后,各個調用release
方法來釋放。
1 2 3 4 5 6 7 8 9 10 11 | { NSString *text = @"Hello, world"; //@"Hello, world"對象的RC=1 NSLog(@"%@", text); NSString *anotherText = text; //@"Hello, world"對象的RC=1 [anotherText retain]; //@"Hello, world"對象的RC=2 NSLog(@"%@", anotherText); [text release]; //@"Hello, world"對象的RC=1 [anotherText release]; //@"Hello, world"對象的RC=0 } |
而使用ARC的話,并不需要調用retain
和release
方法來持有跟釋放對象。
1 2 3 4 5 6 7 8 9 10 | { NSString *text = @"Hello, world"; //@"Hello, world"對象的RC=1 NSLog(@"%@", text); NSString *anotherText = text; //@"Hello, world"對象的RC=2 NSLog(@"%@", anotherText); } /* * 當text和anotherText超過作用域時,會自動調用[text release]和[anotherText release]方法, @"Hello, world"對象的RC=0 */ |
除了當__strong
變量超過作用域時,編譯器會自動加入release
語句來釋放內存,如果你將__strong
變量重新賦給它其他值,那么編譯器也會自動加入release
語句來釋放變量指向之前的對象。例如:
1 2 3 4 5 6 7 8 9 | { NSString *text = @"Hello, world"; //@"Hello, world"對象的RC=1 NSString *anotherText = text; //@"Hello, world"對象的RC=2 NSString *anotherText = @"Sam Lau"; // 由于anotherText對象引用另一個對象@"Sam Lau",那么就會自動調用[anotherText release]方法,使得@"Hello, world"對象的RC=1, @"Sam Lau"對象的RC=1 } /* * 當text和anotherText超過作用域時,會自動調用[text release]和[anotherText release]方法, * @"Hello, world"對象的RC=0和@"Sam Lau"對象的RC=0 */ |
如果變量var被
__strong
修飾,當變量var指向某個對象objc,那么變量var持有某個對象objc的所有權
前面已經提過內存管理的四條規則:
Objective-C對象的動作 | Objective-C對象的方法 |
---|---|
1. 創建一個對象并獲取它的所有權 | alloc/new/copy/mutableCopy (RC = 1) |
2. 獲取對象的所有權 | retain (RC + 1) |
3. 放棄對象的所有權 | release (RC – 1) |
4. 釋放對象 | dealloc (RC = 0 ,此時會調用該方法) |
我們總結一下編譯器是按以下方法來實現的:
__strong
變量來實現,其實編譯器根據__strong
修飾符來管理對象內存。但是__strong
并不能解決引用循環(Reference Cycle)問題:對象A持有對象B,反過來,對象B持有對象A;這樣會導致不能釋放內存造成內存泄露問題。
引用Pro Multithreading and Memory Management for iOS and OS X的圖
舉一個簡單的例子,有一個類Test
有個屬性objc,有兩個對象test1和test2的屬性objc互相引用test1和test2:
1 2 3 4 5 | @interface Test : NSObject @property (strong, nonatomic) id objc; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | { Test *test1 = [Test new]; /* 對象a */ /* test1有一個強引用到對象a */ Test *test2 = [Test new]; /* 對象b */ /* test2有一個強引用到對象b */ test1.objc = test2; /* 對象a的成員變量objc有一個強引用到對象b */ test2.objc = test1; /* 對象b的成員變量objc有一個強引用到對象a */ } /* 當變量test1超過它作用域時,它指向a對象會自動release * 當變量test2超過它作用域時,它指向b對象會自動release * * 此時,b對象的objc成員變量仍持有一個強引用到對象a * 此時,a對象的objc成員變量仍持有一個強引用到對象b * 于是發生內存泄露 */ |
如何解決?于是我們引用一個__weak
ownership qualifier,被它修飾的變量都不持有對象的所有權,而且當變量指向的對象的RC為0時,變量設置為nil。例如:
1 2 | __weak NSString *text = @"Sam Lau"; NSLog(@"%@", text); |
由于text變量被__weak
修飾,text并不持有@"Sam Lau"
對象的所有權,@"Sam Lau"
對象一創建就馬上被釋放,并且編譯器給出警告??,所以打印結果為(null)
。
所以,針對剛才的引用循環問題,只需要將Test
類的屬性objc設置weak修飾符,那么就能解決。
1 2 3 4 5 | @interface Test : NSObject @property (weak, nonatomic) id objc; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 | { Test *test1 = [Test new]; /* 對象a */ /* test1有一個強引用到對象a */ Test *test2 = [Test new]; /* 對象b */ /* test2有一個強引用到對象b */ test1.objc = test2; /* 對象a的成員變量objc不持有對象b */ test2.objc = test1; /* 對象b的成員變量objc不持有對象a */ } /* 當變量test1超過它作用域時,它指向a對象會自動release * 當變量test2超過它作用域時,它指向b對象會自動release */ |
__unsafe_unretained
ownership qualifier,正如名字所示,它是不安全的。它跟__weak
相似,被它修飾的變量都不持有對象的所有權,但當變量指向的對象的RC為0時,變量并不設置為nil,而是繼續保存對象的地址;這樣的話,對象有可能已經釋放,但繼續訪問,就會造成非法訪問(Invalid access)。例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | __unsafe_unretained id obj0 = nil; { id obj1 = [[NSObject alloc] init]; // 對象A /* 由于obj1是強引用,所以obj1持有對象A的所有權,對象A的RC=1 */ obj0 = obj1; /* 由于obj0是__unsafe_unretained,它不持有對象A的所有權,但能夠引用它,對象A的RC=1 */ NSLog(@"A: %@", obj0); } /* 當obj1超過它的作用域時,它指向的對象A將會自動釋放 */ NSLog(@"B: %@", obj0); /* 由于obj0是__unsafe_unretained,當它指向的對象RC=0時,它會繼續保存對象的地址,所以兩個地址相同 */ |
打印結果是內存地址相同:
如果將__unsafe_unretained
改為weak
的話,兩個打印結果將不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | __weak id obj0 = nil; { id obj1 = [[NSObject alloc] init]; // 對象A /* 由于obj1是強引用,所以obj1持有對象A的所有權,對象A的RC=1 */ obj0 = obj1; /* 由于obj0是__unsafe_unretained,它不持有對象A的所有權,但能夠引用它,對象A的RC=1 */ NSLog(@"A: %@", obj0); } /* 當obj1超過它的作用域時,它指向的對象A將會自動釋放 */ NSLog(@"B: %@", obj0); /* 由于obj0是__weak, 當它指向的對象RC=0時,它會自動設置為nil,所以兩個打印結果將不同*/ |
引入ARC之后,讓我們看看autorelease pool有哪些變化。沒有ARC之前的寫法如下:
1 2 3 4 5 6 7 8 9 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // put object into pool id obj = [[NSObject alloc] init]; [obj autorelease]; [pool drain]; /* 超過autorelease pool作用域范圍時,obj會自動調用release方法 */ |
引入ARC之后,寫法比之前更加簡潔:
1 2 3 | @autoreleasepool { id __autoreleasing obj = [[NSObject alloc] init]; } |
相比之前的創建、使用和釋放NSAutoreleasePool
對象,現在你只需要將代碼放在@autoreleasepool
塊即可。你也不需要調用autorelease
方法了,只需要用__autoreleasing
修飾變量即可。
引用Pro Multithreading and Memory Management for iOS and OS X的圖
但是我們很少或基本上不使用autorelease pool。當我們使用XCode創建工程后,有一個app的入口文件main.m
使用了它:
1 2 3 4 5 | int main(int argc, char * argv[]) { @autoreleasepool { return UIapplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } |
有了ARC之后,新的property modifier也被引入到Objective-C類的property,例如:
1 | @property (strong, nonatomic) NSString *text; |
下面有張表來展示property modifier與ownership qualifier的對應關系
Property modifier | Ownership qualifier |
---|---|
strong | __strong |
retain | __strong |
copy | __strong |
weak | __weak |
assign | __unsafe_unretained |
unsafe_unretained | __unsafe_unretained |
要想掌握iOS/OS X的內存管理,首先要深入理解引用計數(Reference Count)這個概念以及內存管理的規則;在沒引入ARC之前,我們都是通過retain
和release
方法來手動管理內存,但引入ARC之后,我們可以借助編譯器來幫忙自動調用retain
和release
方法來簡化內存管理和減低出錯的可能性。雖然__strong
修飾符能夠執行大多數內存管理,但它不能解決引用循環(Reference Cycle)問題,于是又引入另一個修飾符__weak
。被__strong
修飾的變量都持有對象的所有權,而被__weak
修飾的變量并不持有對象所有權。下篇我們介紹使用工具如何解決常見內存問題:懸掛指針和內存泄露。
全能程序員交流QQ群290551701,聚集很多互聯網精英,技術總監,架構師,項目經理!開源技術研究,歡迎業內人士,大牛及新手有志于從事IT行業人員進入!
新聞熱點
疑難解答