概述
在iOS中開發中,我們或多或少都聽說過內存管理。iOS的內存管理一般指的是OC對象的內存管理,因為OC對象分配在堆內存,堆內存需要程序員自己去動態分配和回收;基礎數據類型(非OC對象)則分配在棧內存中,超過作用域就會由系統檢測回收。如果我們在開發過程中,對內存管理得不到位,就有可能造成內存泄露。
我們通常講的內存管理,實際上從發展的角度來說,分為兩個階段:MRC和ARC。MRC指的是手動內存管理,在開發過程中需要開發者手動去編寫內存管理的代碼;ARC指的是自動內存管理,在此內存管理模式下由LLVM編譯器和OC運行時庫生成相應內存管理的代碼。
通篇主要介紹關于內存管理的原理及ARC和MRC環境下編寫代碼實現的差異。
提綱
一 引用計數
二 MRC操作對象的方法
- alloc/new/copy/mutableCopy
- retain
- release
- autorelease
- autorelease pool
三 ARC操作對象的修飾符
- __strong
1.1 strong與變量
1.2 strong與屬性
1.3 strong的實現 - __weak
2.1 weak和循環引用
2.2 weak和變量
2.3 weak的實現
? 2.3.1 weak和賦值
? 2.3.2 weak和訪問 - __unsafe_unretained
- _autoreleasing
具體介紹
一、引用計數
在OC中,使用引用計數來進行內存管理。每個對象都有一個與其相對應的引用計數器,當持有一個對象,這個對象的引用計數就會遞增;當這個對象的某個持有被釋放,這個對象的引用計數就會遞減。當這個對象的引用計數變為0,那么這個對象就會被系統回收。
當一個對象使用完沒有釋放,此時其引用計數永遠大于1。該對象就會一直占用其分配在堆內存的空間,就會導致內存泄露。內存泄露到一定程度有可能導致內存溢出,進而導致程序崩潰。
二、MRC操作對象的方法
1.alloc/new/copy/mutableCopy
1.1 持有調用者自己的對象
在蘋果規定中,使用alloc/new/copy/mutableCopy創建返回的對象歸調用者所有,例如以下MRC代碼:
NSMutableArray *array = [[NSMutableArray alloc] init];/*NSMutableArray類對象A*/
NSLog(@"%p", array);
[array release];//釋放
由于對象A由alloc生成,符合蘋果規定,指針變量array指向并持有對象A,引用計數器會加1。另外,array在使用完對象A后需要對其進行釋放。當調用release后,釋放了其對對象A的引用,計數器減1。對象A此時引用計數值為零,所以對象A被回收。不能訪問已經被回收的對象,會發生崩潰。
1.2 持有非調用者擁有的對象
當持有非調用者自己擁有的對象的時候,例如以下代碼:
id obj = [Person person];
[obj retain];
/*do something*/
[obj release];
此時obj變量獲得但不持有Person類對象,可以通過retain進行持有該對象。當我們使用完該對象,應該調用release方法釋放該對象。
注意:按照蘋果的命名規則,必須是alloc/new/copy/mutableCopy開頭,并且是符合駝峰命名規則生成的對象才歸調用者所有。例如以下的方法,生成的對象不歸調用者所有:
- (id)newarray;
- (id)allocwithInfo;
- (id)coPySomething;
- (id)mutablecopyItem;
2.retain
2.1 retain和屬性
我們可以通過屬性來保存對象,如果一個屬性為強引用,我們就可以通過屬性的實例變量和存取方法來對某個對象進行操作,例如某個屬性的setter方法如下:
- (void)setPerson:(Person *)person {
[person retain];
[_person release];
_person = person;
}
我們通過retain新值,release舊值,再給實例變量更新值。需要注意的一點是:需要先retain新值,再release新值。因為如果新舊值是同一個對象的話,先release就有可能導致該對象被系統回收,再去retain就沒有任何意義了。例如下面這個例子:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong)Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//實例變量持有Person類對象(P對象)。這樣賦值不會調用set方法
_person = [[Person alloc] init];
self.person = _person;//調用set方法
}
- (void)setPerson:(Person *)person {
//release釋放對P對象的引用,P對象引用計數值變為零,則P對象被系統回收
[_person release];
//由于P對象已經被回收,再去retain就容易出問題
[person retain];
_person = person;
}
@end
由于P對象被回收,對應其所分配的內存被置于“可用內存池”中。如果該內存未被覆寫,那么P對象依然有效;如果內存被覆寫,那么實例變量_person就會指向一個被覆寫的未知對象的指針,那么實例變量就變成一個“懸掛指針”。
2.2 retain和數組
如果我們把一個對象加入到一個數組中,那么該數組的addObject方法會對該對象調用retain方法。例如以下代碼:
//person獲得并持有P對象,P對象引用計數為1
Person *person = [[Person alloc] init];//Person類對象生成的P對象
NSMutableArray *array = [NSMutableArray array];
//person被加入到數組,對象P引用計數值為2
[array addObject:person];
此時,對象P被person和array兩個變量同時持有。
3.release
3.1 自己持有的對象自己釋放
當我們持有一個對象,如果在不需要繼續使用該對象,我們需要對其進行釋放(release)。例如以下代碼:
//array獲得并持有NSArray類對象
NSArray *array = [[NSArray alloc] init];
/*當不再需要使用該對象時,需要釋放*/
[array release];
//obj獲得但不持有該對象
id obj = [NSArray array];
//持有對象
[obj retain];
/*當不在需要使用該對象時,需要釋放*/
[obj release];
3.2 非自己持有的對象不要釋放
當我們不持有某個對象,卻對該對象進行釋放,應用程序就會崩潰。
//獲得并持有A對象
Person *p = [[Person alloc] init];//Person類對象A
//p釋放對象A的強引用,對象A所有者不存在
//對象A引用計數為零,所以對象A被回收
[p release];
[p release];//釋放非自己持有的對象
另外,我們也不能訪問某個已經被釋放的對象,該對象所占的堆空間如果被覆寫就會發生崩潰的情況。
4.autorelease
autorelease指的是自動釋放,當一個對象收到autorelease的時候,該對象就會被注冊到當前處于棧頂的自動釋放池(autorelease pool)。如果沒有主動生成自動釋放池,則當前自動釋放池對應的是主運行循環的自動釋放池。在當前線程的RunLoop進入休眠前,就會對被注冊到該自動釋放池的所有對象進行一次release操作。
autorelease和release的區別是:release是馬上釋放對某個對象的強引用;autorelease是延遲釋放某個對象的生命周期。
autorelease通常運用在當調用某個方法需要返回對象的情況下,例如以下代碼:
//外部調用
Person *p = [Person person];
NSLog(@"%p", p);//使用無須retain
//持有則需要retain
[p retain];
_person = p;
[_person release];
//Person類內部定義
+ (id)person {
//創建的Person類對象由person獲得并持有
Person *person = [[Person alloc] init];
//[person release];
[person autorelease];
return person;
}
在外部調用,從方法名person知道,創建的對象由p指針變量獲得但不持有。在函數內部,person獲得并持有了Person類對象,所返回的person對象的引用計數加1。換句話說,調用者需要額外處理這多出來的一個持有操作。另外,我們不能在函數內部調用release,不然對象還沒返回就已經被系統回收。這時候使用autorelease就能很好地解決這個問題。
只要把要返回的對象調用autorelease方法,注冊到自動釋放池就能延長person對象的生命周期,使其在autorelease pool銷毀(drain)前依然能夠存活。
另外,person對象在返回時調用了autorelease方法。該對象已經在自動釋放池中,我們可以直接使用對象p,無須再通過[p retain]訪問;不過,如果要用實例變量持有該對象,則需要對變量p進行一次retain操作,示例變量使用完該對象需要釋放該對象。
5. autorelease pool
5.1 autorelease pool和RunLoop(運行循環)
每條線程都包含一個與其對應的自動釋放池,當某條線程被終止的時候,對應該線程的自動釋放池會被銷毀。同時,處于該自動釋放池的對象將會進行一次release操作。
當應用程序啟動,系統默認會開啟一條線程,該線程就是“主線程”。主線程也有一個與之對應的自動釋放池,例如我們常見的ARC下的main.h文件:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
該自動釋放池用來釋放在主線程下注冊到該自動釋放池的對象。需要注意的是,當我們開啟一條子線程,并且在該線程開啟RunLoop的時候,需要為其增加一個autorelease pool,這樣有助于保證內存的安全。
5.2 autorelease pool和降低內存峰值
當我們執行一些復雜的操作,特別是如果這些復雜的操作要被循環執行,那么中間會免不了會產生一些臨時變量。當被加到主線程自動釋放池的對象越來越來多,卻沒有得到及時釋放,就會導致內存溢出。這個時候,我們可以手動添加自動釋放池來解決這個問題。如以下例子所示:
for (int i = 0; i < largeNumber; i++) {
//創建自動釋放池
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//產生許多被注冊到自動釋放池的臨時對象
id obj = [Person personWithComplexOperation];
//釋放池中對象
[pool drain];
}
如上述例子所示,我們執行的循環次數是一個非常大的數字。并且調用personWithComplexOperation方法的過程中會產生許多臨時對象,所產生的臨時對象有可能會被注冊到自動釋放池中。我們通過手動生成一個自動釋放池,并且在每次循環結束前把該自動釋放池的對象執行release操作釋放掉,這樣就能有效地降低內存的峰值了。
三 ARC操作對象的修飾符
1. __strong
1.1 __strong與變量
在ARC模式下,id類型和OC對象的所有權修飾符默認是__strong。當一個變量通過__strong修飾符來修飾,當該變量超出其所在作用域后,該變量就會被廢棄。同時,賦值給該變量的對象也會被釋放。例如:
{
//變量p持有Person對象的強引用
Person *p = [Person person];
//__strong修飾符可以省略
//Person __strong *p = [Person person];
}
//變量p超出作用域,釋放對Person類對象的強引用
//Person類對象持有者不存在,該對象被釋放
上述例子對應的MRC代碼如下:
{
Person *p = [Person person];
[p retain];
[p release];
}
可以看出,ARC和MRC是對應的。只不過在ARC下,對象“持有”和“釋放”的內存管理代碼交由系統去調用罷了。
1.2 strong與屬性
如果一個屬性的修飾符是strong,對于其實例變量所持有的對象,編譯器會在該實例變量所屬類的dealloc方法為其添加釋放對象的方法。在MRC下,我們的dealloc方法有如:
- (void)dealloc {
[p release];//ARC無效
[super dealloc];//ARC無效
}
在ARC下,我們就無須再去寫這樣的代碼了。另外,在dealloc方法中,ARC只能幫我們處理OC對象。如果實例變量持有類似CoreFoundation等非OC對象,則需要我們手動回收:
- (void)dealloc {
CFRelease(_cfObject);
}
在ARC下,dealloc方法一般用來執行兩個任務。第一個就是手動釋放非OC對象;第二個是接觸監聽。另外,在ARC下我們不能主動調用dealloc方法。因為一旦調用dealloc,對象就不再有效。該方法運行期系統會在合適的時機自動去調用。
1.3 strong的實現
在ARC中,除了會自動調用“保留”和“釋放”方法外,還進行了優化。例如某個對象執行了多次“保留”和“釋放”,那么ARC針對特殊情況有可能會將該對象的“保留”和“釋放”成對地移除。例如:
+ (id)person {
Person *tmp = [[Person alloc] init];//引用計數為1
[tmp autorelease];//注冊到自動釋放池(ARC無效)
return tmp;
}
{
//ARC
_p = [Person person];//_p是強引用屬性對應的實例變量
//實現展示
Person *p = [Person person];//Person類對象引用計數為1
_p = [p retain];//遞增為2
[_p release];//遞減為1
}
//清空自動釋放池,Person類對象遞減為0,釋放該對象
在上述代碼中,ARC對應MRC的具體實現展示。在上面的展示中,+(id)person方法內部會調用一次autorelease操作來延遲其返回對象的生命周期,并且稍后在自動釋放池進行release操作。_p通過retain來持有該對象,使用完就執行release操作。從而看出retain和autorelease是多余的,完全可以簡化成以下代碼:
+ (id)person {
Person *tmp = [[Person alloc] init];//引用計數為1
return tmp;
}
{
//ARC
_p = [Person person];//_p是強引用屬性對應的實例變量
//實現展示
_p = [Person person];//Person類對象引用計數為1
[_p release];//遞減為0,Person類對象被回收
}
那么ARC是如何判斷是否移除這種成對的操作呢?其實在ARC中,并不是直接執行retain和autorelease操作的,而是通過以下兩個方法:
objc_autoreleaseReturnValue(obj);//對應autorelease
objc_retainAutoreleasedReturnValue(obj);//對應retain
以下為兩個方法對應的偽代碼:
id objc_autoreleaseReturnValue(id obj) {
if ("返回對象obj后面的那段代碼是否執行retain") {
//是
set_flag(obj);//設置標志位
return obj;
} else {
return [obj autorelease];
}
}
id objc_retainAutoreleasedReturnValue(obj) {
if (get_flag(obj)) {
//有標志位
return obj;
} else {
return [obj retain];
}
}
通過以上兩段偽代碼,我們重新梳理一下代碼:
+ (id)person {
Person *tmp = [[Person alloc] init];//引用計數為1
return objc_autoreleaseReturnValue(id tmp);
}
{
//ARC
_p = [Person person];//_p是強引用屬性對應的實例變量
//實現展示
Person *p = [Person person];//Person類對象引用計數為1
_p = objc_retainAutoreleasedReturnValue(p);//遞增為1
[_p release];//遞減為0
}
從上述示例代碼可以看出:
當我們用變量p獲取person方法返回的對象前,person方法內部會執行objc_autoreleaseReturnValue方法。該方法會檢測返回對象之后即將執行的那段代碼,如果那段代碼要向所返回的對象執行retain方法,則為該對象設置一個全局數據結構中的標志位,并把對象直接返回。反之,在返回之前把該對象注冊到自動釋放池。
當我們對Person類對象執行retain操作的時候,會執行objc_retainAutoreleasedReturnValue方法。該方法會檢測對應的對象是否已經設置過標志位,如果是,則直接把該對象返回;反之,會向該對象執行一次retain操作再返回。
在ARC中,通過設置和檢測標志位可以移除多余的成對(“保留”&“釋放”)操作,優化程序的性能。
2. __weak
2.1 weak和循環引用
__weak與我們上述所提到的__strong相對應,__strong對某個對象具有強引用。那么,__weak則指的是對某個對象具有弱引用。一般weak用來解決我們開發中遇到的循環引用問題,例如以下代碼:
/*Man類*/
#import <Foundation/Foundation.h>
@class Woman;
@interface Man : NSObject
@property (nonatomic, strong)Woman *person;
@end
/*Woman類*/
#import <Foundation/Foundation.h>
@class Man;
@interface Woman : NSObject
@property (nonatomic, strong)Man *person;
@end
/*調用*/
- (void)viewDidLoad {
[super viewDidLoad];
Man *man = [[Man alloc] init];
Woman *woman = [[Woman alloc] init];
man.person = woman;
woman.person = man;
}
從上述代碼可以看出,Man類對象和Woman類對象分別有一個所有權修飾符為strong的person屬性。兩個類之間互相通過實例變量進行強引用,Man類對象如果要釋放,則需要Woman類對象向其發送release消息。然而Woman類對象要執行dealloc方法向Man類對象發送release消息的話,又需要Man類對象向其發送release消息。雙方實例變量互相強引用類對象,所以造成循環引用,如下圖所示:
這時候weak修飾符就派上用場了,因為weak只通過弱引用來引用某個對象,并不會真正意義上的持有該對象。所以我們只需要把上述兩個類對象的其中一個屬性用weak來修飾,就可以解決循環引用的問題。例如我們把Woman類的person屬性用weak來修飾,分析代碼如下:
/*調用*/
- (void)viewDidLoad {
[super viewDidLoad];
Man *man = [[Man alloc] init];//Man對象引用計數為1
Woman *woman = [[Woman alloc] init];//Woman對象引用計數為1
man.person = woman;//強引用,Woman對象引用計數為2
woman.person = man;//弱引用,Man對象引用計數為1
}
//變量man超出作用域,對Man類對象的強引用失效
//Man類對象持有者不存在,Man類對象被回收
//Man類對象被回收, woman.person = nil;
//Man調用dealloc方法,Woman類對象引用計數遞減為1
//變量woman超出作用域,對Woman類對象強引用失效
//Woman類對象持有者不存在,Woman類對象被回收
從上述示例代碼可以看出,我們只需要把其中一個強引用修改為弱引用就可以打破循環引用。類似的循環引用常見的有block、NSTimer、delegate等,感興趣的自行查閱相關資料,這里不作一一介紹。
另外,基于運行時庫,如果變量或屬性使用weak來修飾,當其所指向的對象被回收,那么會自動為該變量或屬性賦值為nil。這一操作可以有效地避免程序出現野指針操作而導致崩潰,不過強烈不建議使用一個已經被回收的對象的弱引用,這本身對于程序設計而言就是一個bug。
2.2 weak和變量
如果一個變量被__weak修飾,代表該變量對所指向的對象具有弱引用。例如以下代碼:
Person __weak *weakPerson = nil;
if (1) {
Person *person = [[Person alloc] init];
weakPerson = person;
NSLog(@"%@", weakPerson);//weakPerson弱引用
}
NSLog(@"%@", weakPerson);
輸出結果如下:
[1198:73648] <Person: 0x600000018120>
[1198:73648] (null)
從上述輸出結果可以分析,當超出作用域后,person變量對Person對象的強引用失效。Person對象持有者不存在,所以該對象被回收。同時,weakPerson變量對Person的弱引用失效,weakPerson變量被賦值為nil。
另外需要注意的是,如果一個變量被weak修飾,那么這個變量不能持有對象示例,編譯器會發出警告。例如以下代碼:
Person __weak *weakPerson = [[Person alloc] init];
因為weakPerson被__weak修飾,不能持有生成的Person類對象。所以Person類對象創建完立即被釋放,所以編譯器會給出相應的警告:
Assigning retained object to weak variable; object will be released after assignment
2.3 weak的實現
2.3.1 weak和賦值
要解釋weak賦值的實現,我們先看以下示例代碼:
Person *person = [[Person alloc] init];
Person __weak *p = person;
上述對應的模擬代碼如下:
Person *person = [[Person alloc] init];
Person *p;
objc_initWeak(&p, person);
objc_destroyWeak(&p, 0);
上述兩個函數分別用來對變量p的初始化和釋放,它們都調用同一個函數。如下:
id p;
p = 0;
objc_storeWeak(&p, person);//對應objc_initWeak
objc_storeWeak(&p, 0);//對應objc_destroyWeak
根據上述代碼我們進行分析:
1.初始化變量
當我們使用變量弱引用指向一個對象時,通過傳入變量的地址和賦值對象兩個參數來調用objc_storeWeak方法。該方法內部會將對象的地址&person作為鍵值,把變量p的地址&p注冊到weak表中。
2.釋放變量
當超出作用域,Person類對象被回收,此時調用objc_storeWeak(&p, 0)把變量的地址從weak表中刪除。 變量地址從weak表刪除前,利用被回收對象的地址作為鍵值進行檢索,把對應的變量地址賦值為nil。
2.3.2 weak和訪問
當我們訪問一個被__weak修飾過的變量所指向的對象時,其內部是如何實現的?我們先看以下示例代碼:
Person *person = [[Person alloc] init];
Person __weak *p = person;
NSLog(@"%@", p);
上述代碼對應的模擬代碼如下:
Person *person = [[Person alloc] init];
Person *p;
objc_initWeak(&p, person);
id tmp = objc_loadWeakRetained(&p);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&p);
通過上述代碼可以看出,訪問一個被__weak修飾的變量,相對于賦值多了兩個步驟:
- objc_loadWeakRetained,通過該函數取出對應變量所引用的對象并retain;
- 把變量指向的對象注冊到自動釋放池。
這樣一來,weak變量引用的對象就被加入到自動釋放池。在自動釋放池結束前都能安全使用該對象。需要注意的是,如果大量訪問weak變量,就會導致weak變量所引用的對象被多次加入到自動釋放池,從而導致影響性能。如果需要大量訪問,我們通過__strong修飾的變量來解決,例如:
Person __weak *p = obj;
Person *tmp = p;//p引用的對象被注冊到自動釋放池
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
NSLog(@"%@", tmp);
通過利用__strong中間變量的使用,p引用的對象僅注冊1次到自動釋放池中,有效地減少了自動釋放池中的對象。
3. __unsafe_unretained
首先我們需要明確一點,如果一個變量被__unsafe_unretained修飾,那么該變量不屬于編輯器的內存管理對象。該修飾符表明不保留值,即對其所指向的對象既不強引用,也不弱引用。例如以下代碼:
根據上述代碼分析:當超出作用域,變量p對Person類對象的強引用失效。Person類對象的持有者不存在,該對象被回收。當我們再次使用該變量的時候,因為其所表示的對象已經被回收,所以就會發生野指針崩潰。這一點區別于__weak,當被__weak修飾變量所指向的對象被回收,該變量會賦值為nil。
另外,被__unsafe_unretained修飾的變量跟__weak一樣,不能持有對象實例。因為__unsafe_unretained修飾的變量不會對生成的對象實例進行保留操作,所以對象創建完立馬被回收,編譯器會給出相應的警告。
當我們給被__unsafe_unretained修飾的變量賦值時,必須保證賦值對象確實存在,不然程序就會發生崩潰。
4. __autoreleasing
在ARC和MRC中,autorelease作用一致,只是兩者的表現方式有所不同。
1.如果返回的對象歸對用者所有,如下:
@autoreleasepool {
Person __autoreleasing *p = [[Person alloc] init];
}
上述代碼模擬代碼如下:
id pool = objc_autoreleasePoolPush();
Person *p = [[Person alloc] init];//持有
objc_autorelease(p);
objc_autoreleasePoolPop(pool);
2.如果返回的對象不歸調用者所有,如下:
@autoreleasepool {
Person __autoreleasing *p = [Person person];
}
上述代碼模擬代碼如下:
id pool = objc_autoreleasePoolPush();
Person *p = [Person person];
objc_retainAutoreleasedReturnValue(p);//持有(優化后的retain方法)
objc_autorelease(p);
objc_autoreleasePoolPop(pool);
從上面兩個例子可以看出,__autoreleasing的實現都是通過objc_autorelease()函數,只不過是持有方法有所不同而已。
至此,內存管理就介紹完畢了。由于技術有限,難免會存在紕漏,歡迎指正。轉載請注明出處,萬分感謝。
參考資料:
Objective-C 高級編程 iOS和OS X多線程和內存管理
Effective Objective 2.0