可以用@property語法來定義對象中所封裝的數據
通過"特質(arrribute)"來指定存儲數據所需的正確語義
在設置屬性所對應的實例變量時,一定要遵從該屬性所聲明的語義
在開發(fā)iOS程序時應該使用nonatomic屬性,因為atomic會嚴重影響性能
在對象內部盡量直接訪問實例變量
"屬性(property)"是Objective-C的一項特性,用于封裝對象中的數據。Objective-C對象通常會把其所需要的重要數據保存為各種實例變量。實例變量一般通過存取(getter和setter)方法來訪問
在描述個人信息時,通過存放人名,生日,地址等內容,可以在類接口的public區(qū)段聲明一些實例變量
@interface:NSObject{
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someIntenalData;
}
@end
這種編寫風格類似于C++和Java。但是Objective-C幾乎不會這樣做,因為這樣的寫法,對象布局在編譯器就已經確定,當要訪問一個屬性的時候,使用的是硬編碼之后的偏移量(offset),表示該變量存放在對象起始地址有多遠。如果要在_fisrtName前面再加上一個變量,那么之前所指的偏移量都指向了錯誤的地址,修改之后必須重新編譯。

例如,某個代碼庫中的代碼使用了一份舊的類定義。如果與之相連接的代碼使用了新的定義,那么運行的時候就會出現不兼容的現象。各種語言均有相應的解決方法.Objective-C的做法是把存儲偏移量所用的"特殊變量"交給類對象來保存。偏移量會在運行時查找,如果類定義變了,那么存儲的偏移量也就變了,這樣可以保證何時訪問實例變量,總能找到正確的偏移量。甚至可以在運行期向類中新增實例變量,這就是穩(wěn)固的"應用程序二進制接口(Application Binary Interface,ABI)"
正是因為這些特性,使得我們可以在"類擴展(class-continuation)"或實現文件中定義實例變量。所以說,不一定要在接口中把全部實例變量都聲明好,可以將某些變量從接口的public區(qū)段移走,以便保護與類實現有關的內部信息。
盡量不要對象外部直接訪問實例變量,而應該通過存取方法來做(因為運行時機制),可以使用@property語法來編寫實例變量,@property會自動生成對應的存取方法。
例如:
@interface Person:NSObject
@property NSString *firstName;
@end
對于類的使用者來說,相當于
//Person.h
@interface Person:NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString*)firstName;
@end
//Person.m
@implement Person
@synthesize _firstName;
@end
如果讀寫用@property定義的屬性,可以使用點語法,編譯器會自動將點語法轉換為對應的存取方法;
Person *person = [[Person alloc]init];
NSString * temp = person.firstName;
person.firstName = @"Test";
編譯后會變成
Person *person = [[Person alloc]init];
NSString * temp = [person firstName];
[person setFirstName:@"Test"];
屬性特質
使用屬性時還有一個問題需要注意,就是其各種特質(arrribute)設定也會影響編譯器所產生的存取方式,屬性可以擁有的特質分為以下四類
原子性
在默認情況下,由編譯產生的合成方式會通過鎖定機制來確保其原子性(atomic),如果屬性具備nonatomic特質,則不使用同步鎖。
如果開發(fā)iOS應用,你會發(fā)現,其中所有的屬性都聲明為nonatomic。這樣做是因為在開發(fā)iOS中使用同步鎖的開銷較大,這會帶來性能問題,一般情況下并不要求屬性必須是"原子的",因為這并不能保證線程安全,如果要實現線程安全的操作,必須使用更深層次的鎖定機制才可以。也就是說,一個線程在連續(xù)多次讀取某屬性的過程中,有別的線程在同時改寫該值,那么即使將屬性聲明為atomic,也還是會讀到不同的屬性值。因此在開發(fā)iOS程序時,一般都會使用nonatomic屬性。但是在開發(fā)macOS程序的時候,使用atomic一般不會有性能瓶頸。
讀/寫權限
- readwrite(讀寫)特質的屬性擁有獲取(getter)方法和設置(setter)方法,這是默認的讀寫屬性,如果不指明,默認就是readwrite
- readonly只讀,只有讀取方法,沒有對外的設置方法
內存管理語義
屬性用于封裝數據,而數據則要有"具體的所有權語義"
- assign 設置方法只會針對"純量類型"(scalar type,例如CGFloat或者NSInteger等)進行簡單的賦值操作,Objective-C的基本類型和所有在棧上存儲的變量使用這個特質
- strong 表示擁有關系,強引用等同于retain
- weak 表示弱引用,對象不擁有該屬性,對象遭到摧毀的時候,屬性值也會設為nil
- unsafe_unretained 該特質語義和assign類似,但是它適用于"對象類型",該特質表示一種非擁有關系,與weak的區(qū)別在于,對象摧毀的時候,屬性值不會被設置為nil(unsafe)
- copy 此特質表達所屬關系和strong類似,但是是保留一份拷貝的值,NSString一般要使用該特質
方法名
@property默認為屬性生成getter和setter方法名,如果需要自行制定getter和setter方法的方法名,可以使用該特質
- getter = < name >:指定"獲取方法"的方法名,一般屬性是Boolean型,而你想為獲取方法加上"is"前綴,那么可以使用該特質
@property (nonatomic,getter = isOn) BOOL on;
- **setter = < name > **:指定設置方法使用的方法名,不常用
屬性在對象內部的訪問
使用@property生成的變量,在實現文件中會自動生成一個以"_"開頭的同名變量(見上述)。那么,在對象內部是直接訪問還是使用屬性訪問好呢。
例如
@interface Person:NSObject
@property (nonatomic,copy) NSString *firstName;
@property (nonatomic,copy) NSString *lastName;
- (NSString *) fullName;
@end
fullName方法在內部有兩種實現方式
//方法一:使用屬性訪問
- (NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}
//方法二:使用直接訪問
- (NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}
這兩個寫法的區(qū)別在于
- 直接訪問不經過Objective-C的"方法派發(fā)",所以速度更快一些
- 直接訪問實例變量,不會調用其"設置方法",這就繞過了相關屬性所定義的"內存管理語義",比方說,如果在ARC下直接訪問一個聲明為copy的屬性,那么并不會拷貝該屬性,只會保留新值釋放舊值。
- 如果直接訪問實例變量,不會觸發(fā)KVO
- 通過屬性來訪問有助有排查與之相關的錯誤,因為可以給getter和setter設置斷點
可以看出,屬性訪問和直接訪問各有優(yōu)勢,一種折中的方式是:
- 在寫入實例變量的時候,使用屬性訪問
- 在讀取實例變量的時候,直接訪問。
這樣既能提高讀取操作的速度,又能控制對屬性的寫入操作。
需要注意的是,
- 在初始化中應該如何設置屬性值,這種情況下,一般使用直接訪問實例變量來讀寫變量,因為子類可能會overwrite屬性的設置方法。
- 懶加載(lazy initialzation), 在懶加載的情況下,應該使用"獲取方法"來訪問屬性。否則,實例變量就永遠不會被初始化。
懶加載:對于有些變量,如果使用頻率較低,而且創(chuàng)建該變量的成本較高,那么應該使用懶加載,在內部其他地方使用懶加載的變量的時候,不能使用直接訪問的方式來訪問,而是要通過屬性(獲取方法getter)來訪問(注意,只有getter內部可以直接訪問變量),例如,如果Person對象有個brain屬性,懶加載的getter可以這樣寫
- (Brain *)brain
{
if(!_brain){
_brain = [Brain new];
}
return _brain;
}