Objective-C關於非ARC模式下的物件引用計數
Objective-C是一門簡潔、強大、靈活的既具有面向物件特性也具有函數語言程式設計特性的程式語言。由於它是C語言的馬甲,也就是說,Objective-C可以將其原始碼先轉為純C語言然後再編譯為最終的目的碼,所以我們也可以用它來寫純C語言程式碼,它與C是完全相容的!(這點與C++在語法特性上跟C語言相容的特性不同)
由於有不少Objective-C愛好者對於ARC模式下的Objective-C感到十分困惑,所以希望能深入瞭解一下傳統非ARC模式下的程式設計法則。通過對非ARC模式Objective-C工作模式的認知,我們甚至可以對整個Cocoa Framework的執行核心做更深層的認知。為何我不推薦使用ARC模式呢?
你用了ARC就得去記__strong、__weak、__unsafe_unretained、__autoreleasing、__bridge等等雜七雜八的關鍵字~這些亂七八糟的概念本身會把你搞暈,而且當你半懂不懂的時候一旦亂用反而會產生各種奇怪的bug~這些玩意兒倘若充斥在你的程式碼中,一來很醜,二來對於一些新手很容易被弄暈……所以說,ARC這貨自其出生就帶來了許多災難!
而反觀傳統的非ARC模式,property就一個assign,一個retain,NSObject裡就呼叫retain/release和autorelease方法~而且Apple對此的規則也非常簡單——“不是你建立的就不需要你釋放;是你建立的你才去釋放它。”這一句話就能解決所有問題~
除此之外,無論你用ARC還是非ARC,你都需要搞懂Apple Cocoa Framework的訊息迴圈機制,即autorelease是如何工作的。否則你的assign或weak屬性的Objective-C物件啥時候被釋放也都不會知曉~
綜上所述,如果為了程式設計方便、可維護、可擴充套件,我們完全可以把ARC編譯選項給關掉!另外,在Objective-C中往往把“方法呼叫”闡述為“訊息傳送”。比如[obj msg]一般大家描述為obj物件呼叫其msg成員方法。而正式用語上應該描述為向obj物件傳送msg訊息。在哪個物件的方法裡執行這條語句的,那麼稱該物件為訊息傳送者;msg稱為訊息(即方法);obj則稱為訊息接收者。講了那麼多,下面開始切入正題!
在基於Foundation/Cocoa Framework的Objective-C中,我們定義一個類往往需要繼承NSObject這一Foundation的基類。當我們呼叫NSObject的alloc類方法時,就會給要建立的物件分配儲存空間;然後緊接著呼叫NSObject的init成員方法對建立的物件做初始化。這裡就會對此物件做引用計數設定為1的操作。在基於Foundation/Cocoa Framework的Objective-C與傳統的C++不同,它全面通過為每一物件指定引用計數來確定其生命週期。當某個物件的引用計數被減到0時,會觸發呼叫該物件的dealloc成員方法。而上述的init方法就已經把物件的引用計數設定為1了。當我們呼叫NSObject的release成員方法時,該物件的引用計數減1。當我們呼叫NSObject的retain成員方法時,該物件的引用計數加1。通常,我們不要自己去重寫NSObject的release與retain成員方法。下面我們先對此舉一個簡單的例子:
//
// ViewController.m
// iOSTest
//
// Created by Zenny Chen on 15/11/8.
// Copyright © 2015年 GreenGames Studio. All rights reserved.
//
@interface MyObj : NSObject
@end
@implementation MyObj
- (void)dealloc
{
NSLog(@"MyObj deallocated!");
[super dealloc];
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
MyObj *obj = [[MyObj alloc] init];
// 這裡obj的引用計數為1,我們可以將它打印出來,直接訪問retainCount屬性即可。
NSLog(@"retain count: %tu", obj.retainCount);
// 呼叫一次retain,讓其引用計數加1
[obj retain];
NSLog(@"Now, retain count: %tu", obj.retainCount);
// 呼叫一次release,讓其引用計數減1
[obj release];
NSLog(@"After release, retain count: %tu\n", obj.retainCount);
// 再次呼叫release,其引用計數為0,然後dealloc方法被立即觸發
[obj release];
}
通過上述程式碼,我們對retain/release引用計數的機制已經有了非常清晰的理解。不過,如果我們在Objective-C中會時常用到一些Cocoa庫中的物件,包括一些如NSString、NSNumber之類的物件,倘若每分配一個物件都要寫這種release方法進行釋放就會變得非常囉嗦!因而,在基於Foundation/Cocoa Framework的Objective-C中引入了autorelease機制。
要使用autorelease首先需要一個autorelease pool。在Apple LLVM 2.0之前,我們通常需要使用這樣的程式碼開闢一個autorelease pool——
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 在這裡可以對任一繼承NSObject的Objective-C物件使用autorelease
// ...
[pool drain]; // 將所有之前的autorelease物件的引用計數減1
而現在,我們可以直接使用@autoreleasepool:
@autoreleasepool {
// 在這裡可以對任一繼承NSObject的Objective-C物件使用autorelease
// ...
} // 將所有之前的autorelease物件的引用計數減1
當對一個物件呼叫了init方法之後,然後立即再對它呼叫autorelease成員方法,那麼此物件就變為autorelease物件了。此時,其引用計數仍然為1,但是它被其所在的autorelease pool給引用了。此時我們就必須注意,不能通過直接呼叫release方法使其引用計數減到0,否則當autorelease pool再對它做release操作時就會引發程式崩潰,因為該物件已經是一個無效物件了!比如以下程式碼:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
@autoreleasepool {
MyObj *obj = [[[MyObj alloc] init] autorelease];
// 這裡obj的引用計數為1,並且它是一個autorelease物件
NSLog(@"retain count: %tu", obj.retainCount);
// 呼叫一次retain,讓其引用計數加1
[obj retain];
NSLog(@"Now, retain count: %tu", obj.retainCount);
// 呼叫一次release,讓其引用計數減1
[obj release];
NSLog(@"After release, retain count: %tu\n", obj.retainCount);
// 再次呼叫release,其引用計數為0。這裡仍然會觸發dealloc方法的呼叫
[obj release];
NSLog(@"Over");
}// 出了@autoreleasepool語句塊(即呼叫了drain方法之後),之前對obj的引用仍然會對它使用release操作
}
以上程式碼,Over仍然會被列印,但是一旦出了@autoreleasepool語句塊程式即會崩潰~所以,我們把最後一次的 release呼叫給遮蔽掉,這段程式碼就能完好地執行,且不會有任何記憶體洩漏情況。
autorelease pool可以巢狀,即在一個@autoreleasepool語句塊中可再寫一個@autoreleasepool語句塊。那麼內部@autoreleasepool語句塊中定義的autorelease物件就歸內部的autorelease pool管理。
那麼我們現在理解了autorelease的機制,那麼在Foundation/Cocoa Framework中我們如何判定這些類庫中建立的方法是否為autorelease方法呢?很簡單!Apple有一個很基本的命名規則——
1、如果是以alloc類方法分配的,或者是以new打頭的方法獲得的物件說明就是非autorelease物件。Apple稱這些物件為“我們自己建立的物件”,所以最後必須自己負責呼叫release方法去釋放它們。在Core Foundation中,則以CFCreate或含有開頭Create字首的函式名,那麼在使用完這些物件後,我們仍然要以CFRelease相關的函式介面來釋放這些物件。比如以下程式碼:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 使用alloc分配的一個NSString物件
NSString *str = [[NSString alloc] initWithUTF8String:"Hello"];
// 使用CFStringCreateWithCString分配的一個CFStringRef物件
CFStringRef cfStr = CFStringCreateWithCString(kCFAllocatorDefault, ", world",
kCFStringEncodingUTF8);
NSLog(@"String is: %@%@", str, cfStr);
// 用完之後釋放這些物件
[str release];
CFRelease(cfStr);
}
2、如果沒有通過alloc方法獲得的物件,或者是以類名打頭進行初始化後所獲得的物件,那麼它們一般都是autorelease物件。比如以下程式碼:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 使用stringWithUTF8String獲得的一個NSString物件,屬於autorelease物件
NSString *str = [NSString stringWithUTF8String:"Hello, world!"];
// 這裡,str用完後無需自己去release,除非你對它呼叫了retain方法
NSLog(@"string is: %@", str);
}
3、所有字面量都是autorelease物件。比如以下程式碼:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 以下物件均為autorelease物件
NSString *str = @"Hello";
NSString *str2 = @(", world!");
NSNumber *num = @100;
NSArray *array = @[str, str2];
NSDictionary *dict = @{str:num};
// 上述物件用完之後均無需自己呼叫release來釋放,除非你對它們呼叫了retain方法
NSLog(@"String is: %@%@", array[0], array[1]);
NSLog(@"Value is: %@", dict[str]);
}
有了這些簡單的認知之後,我們基本對引用計數有了比較深刻的認識了。由於在Cocoa Framework的訊息機制中,它已經包含了一個autorelease pool,因此,當Cocoa Framework每處理完一個訊息之後就會自動呼叫一次該訊息中所有autorelease物件的release方法。我們還是以剛才定義的MyObj類來舉一個例子:
@implementation ViewController
- (void)test
{
MyObj *obj = [[[MyObj alloc] init] autorelease];
NSLog(@"obj = %@", [obj description]);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self test];
// 在呼叫完我們自己定義的test方法之後,obj物件仍然沒有被釋放
NSLog(@"Over");
// 直到viewDidLoad這一訊息徹底執行完成,
// 我們會看到"MyObj deallocated!"被打印出來
}
所以,以上綜合起來看,就是Apple之前提出的的那句名言——“是你分配的就由你負責釋放,不是你分配就無須你自己釋放!”
最後,我們來談談property以及相關需要注意的事項。在非ARC模式的Objective-C中,property的引用計數屬性也就兩種,1種是assign,另一種就是retain。由於Objective-C編譯器會自動對property生成一個getter方法與一個setter方法(倘若該屬性沒有用readonly修飾)。那麼對於assign限定符來說,setter方法不會對外部引數做retain處理;而對於retain限定符來說,其setter方法就會對外部引數做一次retain處理,使其引用計數加1。我們來看以下程式碼:
@interface ViewController ()
@property (nonatomic, assign) MyObj *objAssign;
@property (nonatomic, retain) MyObj *objRetain;
@end
@implementation ViewController
@synthesize objAssign, objRetain;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
MyObj *obj = [[MyObj alloc] init];
self.objAssign = obj;
// 這裡,obj的引用計數仍然為1
NSLog(@"obj retain count: %tu", obj.retainCount);
self.objRetain = obj;
// 這裡,obj的引用計數變為2
NSLog(@"obj retain count: %tu", obj.retainCount);
}
通過上述程式碼例子,我們可以很清晰地看到retain與assign不同限定符對property的影響。所以,我們通常用的程式設計正規化是:如果你定義的property是retain的,那麼在該類的dealloc方法中對其呼叫release方法;而對於所有NSObject子類而言都推薦優先使用retain限定符;而對於像int、float等非NSObject子類或C語言基本型別而言,對它們用assign修飾。
此外,像Foundation/Cocoa Framework中還有許多方法都自帶retain操作,比如NSMuatbleArray的addObject、UIView的addSubview等等,這些方法都可以通過文件看到其介紹。我們可以通過用option鍵加滑鼠左鍵來看到方法的介紹。比如,像addSubview就有這樣的字眼:“This method establishes a strong reference to view and sets its next responder to the receiver, which is its new superview”。這裡,strong reference就是對引數的強引用,這意味著會對外部引數做引用計數加1操作。一般來說,我們看到以add、insert等詞眼的方法名是就會反應出這些方法會對引數做一次引用計數加1操作;而像remove打頭的方法,我們會反應出這些方法會對訊息接收者做一次引用計數減1的操作。所以,我們一般在寫UI介面程式碼時,要把某些子檢視新增到父檢視上,往往可以採用以下程式設計正規化:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = [UIColor grayColor];
// 這裡通過alloc方法分配了label物件,它不是一個autorelease物件
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(120.0, 50.0, 100.0, 30.0)];
label.textColor = [UIColor whiteColor];
label.text = @"Hello";
// 這裡對label做了一次引用計數加1操作
[self.view addSubview:label];
NSLog(@"label retain count: %tu\n", label.retainCount);
// 由於label是我分配的,所以這裡新增完之後需要對它呼叫一次release方法
[label release];
// 新增一個按鈕
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.frame = CGRectMake(20.0, 50.0, 90.0, 35.0);
[button setTitle:@"Tap" forState:UIControlStateNormal];
[button addTarget:self action:@selector(tapTouched:) forControlEvents:UIControlEventTouchUpInside];
// 由於button用的是buttonWithType獲得的,它是一個autorelease物件而它不是我分配的,
// 所以在新增完之後無需對它使用release方法。
[self.view addSubview:button];
}