【iOS沉思錄】深思Objective-C中的property屬性
OC中的屬性
屬性(Property)是Objective-C語言的其中一個特性,它把類物件中的資料變數及其讀寫方法統一封裝起來,是對傳統C++中要重複為每個變數定義讀寫方法的一種封裝優化,OC將這些變數封裝為屬性變數,系統可自動生成getter和setter讀寫方法,同時仍然允許開發者利用讀寫語義屬性引數(readwrite等)、@synthesize和@dynamic關鍵詞去選擇性自定義讀寫方法或方法名。
迴歸傳統C++類屬性變數的定義形式
原本的類變數定義形式如下,類(Class)是屬性變數和方法的集合,變數的定義可以通過public、private和protected等語義關鍵詞來修飾來限定變數的定義域,實現對變數的封裝,OC中仍然保留這種定義方法,其中關鍵詞改為:@public、@private、@protected以及@package等。
@interface Test : NSObject {
@public // 宣告為共有變數
NSString *_name;
@private // 限制為私有變數
NSString *_major;
@protected // 限制為子類訪問變數,是這裡標頭檔案定義變數的預設型別,如果變數定義在.m實現檔案中預設型別是@private
NSString *_accupation;
@package // 包內變數,只能本框架內使用
NSString *_company;
}
這種傳統定義形式的缺點:
1.每個變數都要手動編寫getter和setter方法,當變數很多時類中會出現大量的這些讀寫方法的程式碼,同時這些讀寫方法的形式是相同的,因此會產生程式碼冗餘。OC中屬性變數的封裝就是將這些方法的定義封裝起來,減少大量形式重複的方法定義;
2.這種類變數定義的方式屬於“硬編碼”,即物件內部的變數定義和佈局已經寫死在了編譯期,編譯後不可再更改,否則會出錯。因為上面所謂的硬編碼,指的是類中的變數會被編譯器定義為離物件初始指標地址的偏移量(offset),編譯之後變數是通過地址偏移來尋找的,如果想要在類中插入新的變數,則必須要重新編譯計算每個變數的偏移量。否則順序被打亂會讀取錯誤的變數。例如下面的例子,在編譯好的物件中變數的前面插入新的變數:
插入後_occupation的偏移量變了,因為現在是第三個指標,這時候按照編譯器的結果訪問就會出錯。
Property屬性變數封裝定義
存取方法和變數名的自動合成:
使用OC的property屬性,編譯器會自動按照OC的嚴格的存取函式命名規範自動生成對應的存取函式,通過存取函式就可以根據變數名訪問對應的變數,通過“點語法”訪問變數其實就是呼叫了變數的存取方法(編譯器會將點語法轉換成存取方法的呼叫),也就是說通過屬性定義的變數名成了存取函式名。此外,還會自動生成對應的例項變數名,由於自定義的變數名跟獲取函式名一樣,為了區分,實際的變數名在前面加下劃線,另外雖然預設是加下劃線,但可以在實現檔案中使用關鍵詞@synthesize自定義實際的變數名。下面例子中使用property屬性定義變數,編譯器會自動生成對應的存取方法和加下劃線的例項變數名,由於是編譯期生成的方法所以編譯之前看不到:
@interface Test : NSObject
// property屬性宣告變數,編譯期到時會自動生成獲取方法:name和設定方法:setName
@property NSString *name;
@end
Test *test = [[Test alloc] init];
// 通過點語法訪問變數,等效於呼叫自動生成的存取方法訪問變數:
// 1.呼叫test的setter方法設定變數
test.name = @"sam";
等效於:
[test setName:@"sam"];
// 2.呼叫test的getter獲取方法訪問變數
NSString *s = test.name;
等效於:
NSString *s = [test name];
// 3.name成了存取方法的函式名,所以要想直接訪問例項變數要使用自動生成的變數名,也就是_name
_name = @"albert";
s = _name;
@synthesize自定義變數名(特殊使用場景):
如果在Test的實現檔案中使用@synthesize關鍵字自定義例項變數名,那麼就不可以通過_name來直接訪問變量了,而是要使用自定義的名字,但實際為了規範和約定,@synthesize自定義例項變數名的用法是不建議使用的(@synthesize的原始用法是和@property成對出現來自動合成指定屬性變數的存取方法的):
@implementation Test
// 自定義例項變數名
@synthesize name = theName;
@end
現在要直接訪問例項變數(不使用存取函式)就要通過自定義的變數名了:
// 通過自定義的變數名訪問,此時_name已經不存在了
theName = @"albert";
s = theName;
【注意:】較舊版本@synthesize和@property是成對出現的,也就是說要手動使用@synthesize來合成相應的存取方法,否則不會自動合成(現在編譯器預設會自動新增@synthesize自動合成存取方法)。
@synthesize name; // 舊版本手動指定要合成存取方法的變數
此時set方法名為:setName, 變數名和get方法名都為name,即name作為方法呼叫就是方法名,作為變數直接取就是變數名:
name = @"Sam"; //name為變數名
NSString *oldName = [self name]; //name為get方法名
@dynamic禁止存取方法自動合成:
@dynamic關鍵字是用來明確告訴編譯器禁止自動合成屬性變數的存取方法。預設情況如果不用@dynamic關鍵字,編譯器就會自動合成那些沒有定義的存取方法,假設程式設計師自定義了setter方法,那麼編譯器就會只自動生成getter方法。
@implementation Test
// 禁止編譯器自動生成存取方法
@dynamic name;
@end
此時,如果程式碼中依舊使用點方法,或者通過存取函式呼叫來訪問name,編譯之前並沒有異常,但編譯之後由於並自動沒有生成存取方法,執行起來時會在存取方法呼叫的位置處程式崩潰,因為呼叫了不存在的方法。
不同屬性特質修飾詞的限制
通過在@property後的括號內新增屬性特質引數,也可以影響存取方法的生成:
@interface Test : NSObject
// 括號內新增屬性特質進行限制
@property(nonatomic, readwrite, copy) NSString *name;
@end
屬性引數主要可以分為三類:
- 原子性: atomic,nonatomic
- 讀寫語義:readwrite,readonly,getter,setter
- 記憶體管理語義:assign,weak,unsafe_unretained,retain,strong,copy
其中最重要的是記憶體管理語義,要理解記憶體管理語義的作用和用法,首先要理解記憶體管理中的引用計數原理,也就是要理解OC的記憶體管理機制,屬性引數的記憶體管理語義是OC中協助管理記憶體的很重要一部分。各種屬性引數的含義和區別如下:
- atomic、nonatomic: 原子性和非原子性。原子性是資料庫原理裡面的一個概念,ACID中的第一個。在多執行緒中同一個變數可能被多個執行緒訪問甚至更改造成資料汙染,因此為了安全,OC中預設是atomic,會對setter方法加鎖,相應的也會付出維護原子性(資料加鎖解鎖等)的系統資源代價。應用中如果不是特殊情況(多執行緒間的通訊程式設計),一般還是用nonatomic來修飾變數的,不會對setter方法加鎖,以提高多執行緒併發訪問時的效能。
- readonly、readwrite: readonly表示變數只讀,也就是它修飾的變數只有get方法沒有set方法;readwrite就是既有get方法也有set方法了,可讀亦可寫;
- getter = < gettername >, setter = < settername >: 可以選擇性的在括號裡直接指定存取方法的方法名,例如:
// 更改預設的獲取方法name為getName
@property(nonatomic, getter=getName, copy) NSString *name;
// 之後要呼叫獲取方法應使用上面指定的
s = [test getName];
- assign:
直接簡單賦值,主要用於修飾基礎資料型別(例如NSInteger)和C資料型別(int, float, double, char等)上,或修飾對指標的弱引用; - weak:
主要可以用於避免迴圈引用,和strong/retain對應,功能上和assign一樣簡單,但不同的是用weak修飾的物件消失後會自動將指標置nil,防止出現‘懸掛指標’; - unsafe_unretained:這種修飾方式不常用,通過名字看出它是不安全的,為什麼這麼說呢?首先它和weak類似都是自己建立並持有的物件之後卻不會繼續被自己持有(引用計數沒有+1,引用計數為0的時候會被自動釋放,儘管unsafe_unretained和weak修飾的指標還指向那個物件)。不同的是雖然在ARC中由編譯器來自動管理記憶體,但unsafe_unretained修飾的變數並不會被編譯器進行記憶體管理,也就是說既不是強引用也不是弱引用,生成的物件立刻就被釋放掉了,也就是出現了所謂的‘懸掛指標’,所以不安全。
- retain:
常用於引用型別,是為了持有物件,宣告強引用,將指標本來指向的舊的引用物件釋放掉,然後將指標指向新的引用物件,同時將新物件的索引計數加1; - strong:
原理和retain類似,只不過在使用ARC自動引用計數時,用strong代替retain; - copy:
建立一個和新物件內容相同且索引計數為1的物件,指標指向這個物件,然後釋放指標之前指向的舊物件。NSString變數一般都用copy修飾,因為字串常用於直接複製,而不是去引用某個字串;
【補充:】除了在屬性變數前面加修飾詞,開發中還會用到一些所有權修飾符,例如: __strong和 __weak。所有權修飾符和上面的修飾符有著對應關係,使用的目的和原理是一樣的,可結合理解記憶,他們的對應關係如下:
- __strong修飾符對應於上面的strong和retain還有copy,強引用來持有物件,它和C++中的智慧指標std::shared_ptr類似,也是通過引用計數來持有例項物件;
- __weak修飾符對應於上面的weak,同樣它和C++中的智慧指標std::weak_ptr類似,也是用於防止迴圈引用問題;
- __unsafe __unretained修飾符對應於上面的assign和unsafe_unretained,建立但不持有物件,可能導致指標懸掛。
相關問題
問題: 區分‘assign’和‘retain’關鍵字的不同以及‘assign’和‘weak’的不同?
‘assign’和‘weak’的區別主要是‘weak’修飾的變數在物件釋放時會自動將變數指標置nil,防止指標懸掛。
問題: atomic原子性屬性和nonatomic非原子性屬性有什麼不同?預設的是哪一個?
atomic原子屬性修飾的變數setter方法會加鎖,防止多執行緒環境下被多個執行緒同時訪問造成資料汙染,但會浪費資源;而nonatomic非原子性屬性修飾的變數setter方法不會被加鎖。為了安全,預設的是atomic原子屬性的。
問題: ARC下,不顯式指定任何屬性關鍵字時,預設的關鍵字都有哪些?
預設的屬性關鍵字分兩種情況:一種是基本資料型別,一種是OC普通物件。不管哪種情況預設都有atomic原子屬性和readwrite可讀寫屬性,區別是基本資料型別預設是有個assign屬性關鍵字,而OC物件對應的預設有個strong屬性關鍵字。
基本資料型別的預設關鍵字有:atomic,readwrite,assign
普通OC物件的預設關鍵字有:atomic,readwrite,strong
問題:
什麼是“強引用”和“弱引用”?為什麼他們很重要以及它們是怎樣幫助控制記憶體管理和避免記憶體洩漏的?
預設的指向物件的指標變數都是strong強引用,當兩個或多個物件互相強引用的時候就可能出現迴圈引用的情況,也就是引用成了一個環狀。例如在ARC自動引用計數機制下迴圈引用中的所有物件將永遠得不到釋放銷燬而導致記憶體洩漏,因為引用迴圈使得裡面的物件的引用計數至少為1(當應用中的所有其他物件都釋放了對環內的這些物件的擁有權的時)。因此物件之間互相的強引用是要儘可能的避免的,使用weak修飾的弱引用就是為了打破迴圈引用從而避免記憶體洩漏的。
問題:
@synthesize和@dynamic各表示什麼,有什麼不同?
@synthesize 修飾的屬性預設情況下由系統自動生成setter和getter方法,除非開發者自己定義了這些方法;而現在@synthesize主要是用來更改屬性變數名的。
@dynamic 用來明確禁止屬性存取方法的自動合成,由程式設計師自己手動編寫存取方法。
問題:
類變數的@protected,@private,@public,@package宣告各有什麼含義?
前三個跟一般面向物件裡面的繼承封裝概念相同:
@protected: 表示變數對子類可見,而對於其他類來說變數是私有的,不可訪問;
@private: 表示變數完全私有化,只對本類可見,其子類也不可訪問;
@public: 公開變數,表示變數對所有類都是開放可見的,都可以訪問;
最後一個就是Objective-C中特有的一個修飾詞了,一般在開發靜態類庫的時候會用到,意思是這個關鍵詞修飾的變數對於framework包內部來說是@protected型別的,而對於包外來說是@priviate型別的,這樣可以實現包內變數的封裝,包內可以使用而包外不可用,防止使用該包的人看到這些變數。
問題:
這段程式碼有什麼問題:
@implementation Person
- (void)setAge:(int)newAge {
self.age = newAge;
}
@end
set方法裡面又巢狀呼叫set方法導致死迴圈。
問題:
在一個物件的方法裡面:self.name = @”object”;和 name = @”object”;有什麼不同?
前者是呼叫set方法賦值,後者是變數直接賦值。第一種情況的程式碼等效於:[self setName:@"object];
問題:
__block 和 __weak 修飾符的區別?
__block: 可以用在ARC和MRC中,可以在MRC中避免迴圈引用問題但在ARC中不可以,可以修飾物件和基本資料型別,在block中可以被重新賦值。
__weak: 只能在ARC中使用,可以用於避免迴圈引用問題,只能修飾物件,不能修飾基本資料型別,不能在block中被重新賦值。
問題:
定義屬性時,什麼情況使用copy、assign、retain?
assign用於簡單資料型別,如NSInteger,double,bool等等。retain和copy用於物件,copy用於當a指向一個物件,b也想指向同樣內容的物件但實際不是同一個物件的時候,如果用assign,a如果釋放,再呼叫b會crash,如果用copy的方式,a和b各自有自己的記憶體,就可以解決這個問題。retain會使計數器加一,也可以解決assign的問題。
問題:
分別寫一個setter方法用於完成非ARC下的@property(nonatomic,retain)NSString *name
和@property(nonatomic,copy)NSString *name
第一種情況retain是指標變數name對新賦值物件的強引用,相當於ARC下的strong,因此對name指標變數set新值時要先將新賦值物件的引用計數加1,然後將指標變數指向新賦值物件,類似於‘淺拷貝’。
// retain
- (void)setName:(NSString *)newName {
[newName retain]; // 新物件引用計數加1
[name release]; // 將指標變數原來的物件釋放掉
name = newName; // 指標變數指向新物件
}
第二種情況copy指的是對指標變數name賦值新物件時,是將新物件完全copy一份,將copy好的物件複製給指標變數,即指標指向的是臨時copy出來的物件,而不是新賦值的那個物件,因此新賦值物件不需要引用計數加1,因為指標變數並沒有指向持有它,類似於‘深拷貝’。
// copy
- (void)setName:(NSString *)newName{
id temp = [newName copy]; // 將新物件原樣克隆一份
[name release]; // 將指標變數原來的物件釋放掉
name = temp; // 指標變數指向新物件的克隆體
}
問題:如何僅僅通過屬性特質的引數來實現公有的getter函式和私有的setter函式?
首先如果不考慮自動合成的功能,如果要手動寫一個共有的getter函式那麼我們先要在.h標頭檔案中宣告這個getter函式以暴露給外部呼叫,並在.m檔案中進行實現,然後手動在.m檔案中寫一個私有的setter函式的實現即可,當然私有函式可以在.m的continue區域進行私有函式宣告,但是沒有必要,只要不在.h檔案中宣告暴露即可(C++中是要在.m檔案最前面宣告的,否則要考慮函式呼叫順序,在函式實現之前無法呼叫)。這裡以一個簡單的Person類為例說明具體寫法,手動實現的方法如下:
/** .h標頭檔案區域 **/
@interface Person : NSObject {
NSString *name;
}
- (NSString *)name; // 宣告公有的getter函式
@end
#import "Person.h"
/** continue 私有宣告區域 **/
@interface Person()
- (void)setName:(NSString *)newName; // 在.m檔案的continue區域宣告私有setter方法,通常私有函式不需要宣告,可以省略
@end
/** implementation實現區域 **/
@implementation Person
/**
* 公有的getter函式實現
*/
- (NSString *)name {
return name;
}
/**
* 私有的setter函式實現
*/
- (void) setName:(NSString *)newName {
name = newName;
}
@end
這樣在類外部是可以呼叫getter方法的,但setter方法只能在本類內部呼叫,外部無法找到setter方法。
現在題目要求我們使用屬性的讀寫語義也就是readwrite和readonly來讓編譯器自動合成上面的效果,如何實現呢?
實現方法是要在.h標頭檔案和.m實現檔案中定義屬性變數兩次,第一次在.h標頭檔案中使用readonly讀寫語義讓編譯器自動合成公有的getter函式,第二次在.m檔案中使用readwrite讀寫語義再讓編譯器自動合成私有的setter方法。寫法如下:
/** .h標頭檔案區域 **/
@interface Person : NSObject
@property (nonatomic, readonly, copy)NSString *name; // 使用readonly,讓編譯器只合成公有getter方法
@end
/** continue 私有宣告區域 **/
@interface Person()
@property (nonatomic, readwrite, copy) NSString *name; // 讓編譯器再合成私有setter方法,其中readwrite可以省略,因為預設就是readwrite
@end
/** implementation實現區域 **/
@implementation Person
/**
* 測試
*/
- (void)test {
// 下面兩條語句等效,都是呼叫setter方法,但注意setter方法是私有的,只能在此處呼叫,在外部無法呼叫
self.name = @"name";
[self setName:@"name"];
}
@end