你要知道的@property都在這里
轉(zhuǎn)載請注明出處 http://www.itdecent.cn/p/646ae400fe7b
本文大綱
- Apple Adopting Modern Objective-C翻譯
- @property基本用法
- @property修飾符詳解
- @property進階話題: 深入代碼理解
Apple在Adopting Modern Objective-C一文中介紹了現(xiàn)代化OC的寫法,其中就介紹盡量使用@property定義類的屬性,先來看看蘋果是怎么介紹property的。
Apple Official Property Introduction
Objective-C的屬性(property)是通過用@property定義的公有或私有的方法。例如:
@property(readonly, getter=isBlue) BOOL blue;
屬性捕獲了對象的狀態(tài)。它們反映了對象的固有屬性(intrinsic attributes)以及對象與其他對象之間的關(guān)系。屬性(property)提供了一種安全、便捷的方式來與這些屬性(attribute)交互,而不需要手動編寫一系列的訪問方法,如果需要的話可以自定義getter和setter方法來覆蓋編譯器自動生成的相關(guān)方法。
盡量多的使用屬性(property)而不是實例變量(attribute)因為屬性(property)相比實例變量有很多的好處:
- 自動合成getter和setter方法。當聲明一個屬性(property)的時候編譯器默認情況下會自動生成相關(guān)的getter和setter方法
- 更好的聲明一組方法。因為訪問方法的命名約定,可以很清晰的看出getter和setter的用處。
- 屬性(property)關(guān)鍵詞能夠傳遞出相關(guān)行為的額外信息。屬性提供了一些可能會使用的特性來進行聲明,包括
assign(vscopy),weak,strong,atomic(vsnonatomic),readwrite,readonly等。
屬性方法遵守一個簡單的命名約定。getter的名字與屬性名相同(如:屬性名為date則getter的名字也為date),setter的名字則是屬性名字加上set前綴并采用駝峰命名規(guī)則(如:屬性名為date則setter的名字為setDate)。布爾類型的屬性還可以定義一個以is開頭的getter方法,如:
@property (readonly, getter=isBlue) BOOL blue;
如果按照上面的方法聲明則以下所有訪問方式都正確:
if (color.blue) {}
if (color.isBlue) {}
if ([color isBlue]) {}
當決定什么東西可以作為一個屬性的時候,需要注意以下這些不屬于屬性:
- init方法
- copy和mutableCopy方法
- 類工廠方法
- 開啟某項操作并返回一個BOOL結(jié)果的方法
- 明確的改變了一個getter的內(nèi)部狀態(tài)的副作用方法
除此之外,在你的代碼中使用屬性特性的時候請考慮以下規(guī)則:
- 一個可讀寫(read/write)的屬性有兩個訪問方法。setter方法是有一個參數(shù)的無返回值方法,getter方法是沒有參數(shù)的且有一個返回值的方法,返回值類型與屬性聲明的類型一致。如果將這組方法轉(zhuǎn)換成一個屬性,就可以用readwrite關(guān)鍵字來標記它(默認即為
readwrite可不寫)。 - 一個只讀(read-only)的屬性只有一個訪問方法。即getter方法,它不接受任何參數(shù),并且返回一個值。如果將這個方法轉(zhuǎn)換成一個屬性,就可以用readonly關(guān)鍵字標記它。
- getter方法應(yīng)當是冪等(idempotent)的(如果一個getter方法被調(diào)用兩次,那么第二次調(diào)用時返回的結(jié)果應(yīng)該和第一調(diào)用時返回的結(jié)果相同)。然而,如果一個getter方法每次調(diào)用時,是被用于計算結(jié)果,這是可以接受的。
如何適配
識別出一組可以被轉(zhuǎn)換成一個屬性的方法,如這些方法:
- (NSColor *)backgroundColor;
- (void)setBackgroundColor:(NSColor *)color;
用@property語法和適當?shù)年P(guān)鍵字將它們定義成一個屬性:
@property (copy) NSColor *backgroundColor;
有關(guān)屬性關(guān)鍵詞和其他注意事項,可以閱讀Encapsulating Data。
或者,你也可以使用Xcode中的modern Objective-C轉(zhuǎn)換器來自動轉(zhuǎn)換你的代碼。參考Refactoring Your Code Using Xcode。
@property基本用法
手工創(chuàng)建getter與setter
@interface Person : NSObject
{
NSString *_name;
NSUInteger _age;
}
- (void)setName:(NSString*)name;
- (NSString*)name;
- (void)setAge:(NSUInteger)age;
- (NSUInteger)age;
@end
@implementation Person
- (void)setName:(NSString*)name {
_name = [name copy];
}
- (NSString*)name {
return _name;
}
- (void)setAge:(NSUInteger)age {
_age = age;
}
- (NSUInteger)age {
return _age;
}
@end
上述代碼就是手動創(chuàng)建變量的getter和setter的實現(xiàn),getter和setter本質(zhì)就是符合一定命名規(guī)范(前文Apple Official Property Introduction有講解)的實例方法。
具體使用方法如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//函數(shù)調(diào)用name的setter
[p setName:@"Jiaming Chen"];
//函數(shù)調(diào)用age的setter
[p setAge:22];
//函數(shù)調(diào)用name和age的getter,輸出 Jiaming Chen 22
NSLog(@"%@ %ld", [p name], [p age]);
}
return 0;
}
通過調(diào)用方式可以看出,setter和getter本質(zhì)就是實例方法,可以通過函數(shù)調(diào)用的方式來使用。
為了方便使用,Objective-C允許使用點語法來訪問getter和setter。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//使用點語法訪問name的setter
p.name = @"Jiaming Chen";
//使用點語法訪問age的setter
p.age = 22;
//使用點語法訪問name和age的getter,輸出 Jiaming Chen 22
NSLog(@"%@ %ld", p.name, p.age);
}
return 0;
}
使用點語法訪問的方式本質(zhì)還是調(diào)用了我們手動創(chuàng)建的setter和getter。
當有很多變量需要設(shè)置時,這樣手工創(chuàng)建setter和getter的方式難免很繁瑣,因此合成存取方法就誕生了。
合成存取方法
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
在聲明一個屬性(property)的時候盡量使用Foundation框架的數(shù)據(jù)類型,如整形使用NSInteger或NSUInteger表示,時間間隔的浮點類型使用NSTimeInterval表示,這樣代碼數(shù)據(jù)類型更統(tǒng)一。
上面的代碼使用@property聲明兩個屬性name和age并為其設(shè)置了一些指示符(nonatomic,copy,assign等,下文會詳細介紹)。
@synthesize表示為這兩個屬性自動生成名為_name和_age的底層實例變量,并自動生成相關(guān)的getter和setter也可以不寫編譯器默認會自動生成'_屬性名'的實例變量以及相關(guān)的getter和setter。
這里所說的編譯器自動生成的實例變量就如同我們在上文中手動創(chuàng)建setter和getter時聲明的變量_name和_age。也就是說編譯器會在編譯時會自動生成并使用_name和_age這兩個變量來存儲這兩個屬性,跟name和age沒什么關(guān)系了,只是我們在上層使用這兩個屬性的時候可以用name和age的點語法來訪問getter和setter。如果不想使用這兩個名字用于底層的存儲也可以任意命名,但最好按照官方的命名原則來命名。
也可以自定義getter和setter方法來覆蓋編譯器默認生成的方法,就如同手動創(chuàng)建getter和setter一樣。
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
//編譯器會幫我們自動生成_name和_age這兩個實例變量,下面代碼就可以正常使用這兩個變量了
@synthesize name = _name;
@synthesize age = _age;
- (void)setName:(NSString*)name {
//必須使用_name來賦值,使用self.name來設(shè)置值時編譯器會自動轉(zhuǎn)為調(diào)用該函數(shù),會導致無限遞歸
//使用_name則是直接訪問底層的存儲屬性,不會調(diào)用該方法來賦值
//這里使用copy是為了防止NSMutableString多態(tài)
_name = [name copy];
}
- (NSString*)name {
//必須使用_name來訪問屬性值,使用self.name來訪問值時編譯器會自動轉(zhuǎn)為調(diào)用該函數(shù),會造成無限遞歸
return _name;
}
@end
使用自定義的getter和setter一般是用來實現(xiàn)懶加載(lazy load),在很多情況下很常用,比如:創(chuàng)建一個比較大的而又不一定會使用的對象,可以按照如下方法編寫。
@property (nonatomic, strong) CustomObject *customObject;
@synthesize customObject = _customObject;
- (CustomObject*) customObject {
if (_customObject == nil) {
//初始化操作,會調(diào)用setter方法
self.customObject = [[CustomObject alloc] init];
//如果按照如下方法編寫不會調(diào)用setter方法,如果自定義setter方法需要完成一些事情建議使用self.customObject的方式來設(shè)置
//_customObject = [[CustomObject alloc] init];
}
return _customObject;
}
@property指示符
在聲明屬性的時候一般會帶上幾個指示符,常用指示符有
atomic nonatomicreadwrite readonlyassignstrongweakcopyunsafe_unretainedretain
還可以設(shè)置getter和setter對其重命名,這里不再贅述。
atomic/nonatomic
指定合成存取方法是否為原子操作,可以理解為是否線程安全,但在iOS上即時使用atomic也不一定是線程安全的,要保證線程安全需要使用鎖機制,超過本文的講解范圍,可以自行查閱。
可以發(fā)現(xiàn)幾乎所有代碼的屬性設(shè)置都會使用nonatomic,這樣能夠提高訪問性能,在iOS中使用鎖機制的開銷較大,會損耗性能。
readwrite/readonly
readwrite是編譯器的默認選項,表示自動生成getter和setter,如果需要getter和setter不寫即可。
readonly表示只合成getter而不合成setter。
assign、weak、unsafe_unretained
assign表示對屬性只進行簡單的賦值操作,不更改所賦的新值的引用計數(shù),也不改變舊值的引用計數(shù),常用于標量類型,如NSInteger,NSUInteger,CGFloat,NSTimeInterval等。
assign也可以修飾對象如NSString等類型對象,上面說過使用assign修飾不會更改所賦的新值的引用計數(shù),也不改變舊值的引用計數(shù),如果當所賦的新值引用計數(shù)為0對象被銷毀時屬性并不知道,編譯器不會將該屬性置為nil,指針仍舊指向之前被銷毀的內(nèi)存,這時訪問該屬性會產(chǎn)生野指針錯誤并崩潰,因此使用assign修飾的類型一定要為標量類型。
@interface Person : NSObject
@property (nonatomic, assign) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//這里使用NSMutableString而不使用NSString是因為NSString會緩存字符串,后面置空的時候?qū)嶋H沒有被銷毀
NSMutableString *s = [[NSMutableString alloc] initWithString:@"Jiaming Chen"];
//設(shè)置p.name不會增加s的引用計數(shù),只是單純將s指向的地址賦給p.name
p.name = s;
//輸出兩個變量的內(nèi)存地址,可以看出是一致的
NSLog(@"%p %p", p.name, s);
//這里可以正常訪問name
NSLog(@"%@ %ld", p.name, p.age);
//將上述字符串置空,引用計數(shù)為0,對象被銷毀
s = nil;
//查看其地址時仍然可以訪問到,表示其仍然指向那一塊內(nèi)存
NSLog(@"%p", p.name);
//訪問內(nèi)容時發(fā)生野指針錯誤,程序崩潰。因為對象已經(jīng)被銷毀
NSLog(@"%@ %ld", p.name, p.age);
}
return 0;
}
使用weak修飾的時候同樣不會增加所賦的新值的引用計數(shù),也不減少舊值的引用計數(shù),但當該值被銷毀時,weak修飾的屬性會被自動賦值為nil,這樣就可以避免野指針錯誤。
使用unsafe_unretained修飾時效果與assign相同,不會增加引用計數(shù),當所賦的值被銷毀時不會被置為nil可能會發(fā)生野指針錯誤。unsafe_unretained與assign的區(qū)別在于,unsafe_unretained只能修飾對象,不能修飾標量類型,而assign兩者均可修飾。
為了防止多態(tài)的影響,對NSString進行修飾時一般使用copy。
下文會對weak、unsafe_unretained和copy進行詳細介紹。
strong、weak
strong表示屬性對所賦的值持有強引用表示一種“擁有關(guān)系”(owning relationship),會先保留新值即增加新值的引用計數(shù),然后再釋放舊值即減少舊值的引用計數(shù)。只能修飾對象。如果對一些對象需要保持強引用則使用strong。
weak表示對所賦的值對象持有弱引用表示一種“非擁有關(guān)系”(nonowning relationship),對新值不會增加引用計數(shù),也不會減少舊值的引用計數(shù)。所賦的值在引用計數(shù)為0被銷毀后,weak修飾的屬性會被自動置為nil能夠有效防止野指針錯誤。
weak常用在修飾delegate等防止循環(huán)引用的場景。
copy
copy修飾的屬性會在內(nèi)存里拷貝一份對象,兩個指針指向不同的內(nèi)存地址。
一般用來修飾有對應(yīng)可變類型子類的對象。
如:NSString/NSMutableString,NSArray/NSMutableArray,NSDictionary/NSMutableDictionary等。
為確保這些不可變對象因為可變子類對象影響,需要copy一份備份,如果不使用copy修飾,使用strong或assign等修飾則會因為多態(tài)導致屬性值被修改。
這里的copy還牽扯到NSCopying和NSMutableCopying協(xié)議,在下文會有簡要介紹。
@interface Person : NSObject
//使用strong修飾NSString
@property (nonatomic, strong) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s = [[NSMutableString alloc] initWithString:@"Jiaming Chen"];
//將可變字符串賦值給p.name
p.name = s;
//輸出的地址和內(nèi)容均一致
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
//修改可變字符串s
[s appendString:@" is a good guy"];
//再次輸出p.name被影響
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
}
return 0;
}
copy還被用來修飾block,在ARC環(huán)境下編譯器默認會用copy修飾, 一般情況下在block需要捕獲外界數(shù)據(jù)時該block就會被分配在堆區(qū),但在MRC環(huán)境下由于手動管理引用計數(shù),block一般被分配在棧區(qū),需要copy到堆區(qū)來防止野指針錯誤。由于牽扯block相關(guān)知識,有興趣可以看博客另一篇文章iOS block探究(二): 深入理解
對于可變對象類型,如NSMutableString、NSMutableArray等則不可以使用copy修飾,因為Foundation框架提供的這些類都實現(xiàn)了NSCopying協(xié)議,使用copy方法返回的都是不可變對象,如果使用copy修飾符在對可變對象賦值時則會獲取一個不可變對象,接下來如果對這個對象進行可變對象的操作則會產(chǎn)生異常,因為OC沒有提供mutableCopy修飾符,對于可變對象使用strong修飾符即可。具體栗子如下:
@interface Person : NSObject
//使用copy修飾NSMutableString
@property (nonatomic, copy) NSMutableString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s = [[NSMutableString alloc] initWithString:@"Jiaming Chen"];
//將可變字符串賦值給p.name
p.name = s;
//輸出的地址不一致,內(nèi)容一致
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
//修改p.name,此時拋出異常
[p.name appendString:@" is a good guy."];
}
return 0;
}
上面的栗子使用copy修飾可變對象,在進行賦值的時候會通過copy方法獲取一個不可變對象,因此p.name的地址和s的地址不同,而p.name運行時類型為NSString,調(diào)用appendString:方法會拋出異常。
所以,針對不可變對象使用copy修飾,針對可變對象使用strong修飾。
unsafe_unretained
使用unsafe_unretained修飾時效果與assign相同,不會增加新值的引用計數(shù),也不會減少舊值的引用計數(shù)(unretained)當所賦的值被銷毀時不會被置為nil可能會發(fā)生野指針錯誤(unsafe)。unsafe_unretained與assign的區(qū)別在于,unsafe_unretained只能修飾對象,不能修飾標量類型,而assign兩者均可修飾。
retain
在ARC環(huán)境下使用較少,在MRC下使用效果與strong一致。
copy的題外話
有時候我們需要copy一個對象,或是mutableCopy一個對象,這時需要遵守NSCopying和NSMutableCopying協(xié)議,來實現(xiàn)copyWithZone:和mutableCopyWithZone:兩個方法,而不是重寫copy和mutableCopy兩個方法。
Foundation框架中的很多數(shù)據(jù)類型已經(jīng)幫我們實現(xiàn)了上述兩個方法,因此我們可以使用copy方法和mutableCopy方法來復制一個對象,兩者的區(qū)別在于copy的返回值仍未不可變對象,mutableCopy的返回值為可變對象。
| type | copy | mutableCopy |
|---|---|---|
| NS* | 淺拷貝,只拷貝指針,地址相同 | 單層深拷貝,拷貝內(nèi)容,地址不同 |
| NSMutable* | 單層深拷貝,拷貝內(nèi)容,地址不同 | 單層深拷貝,拷貝內(nèi)容,地址不同 |
由上述表格可以看出,對于不可變類型,使用copy方法時是淺拷貝,只拷貝指針,因為內(nèi)容是不會變化的。使用mutableCopy時由于返回可變對象因此需要一份拷貝,供其他對象使用。對于可變類型,不管是copy還是mutableCopy均會進行深拷貝,所指向指針不同。
前文介紹copy修飾符的時候講過,在修飾NSString這樣的不可變對象的時候使用copy修飾,但其實當給對象賦一個NSString時仍舊只復制了指針而不是拷貝內(nèi)容,原因同上。
@interface Person : NSObject
//使用copy修飾
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSString *s = @"Jiaming Chen";
p.name = s;
//p.name的地址與s地址相同,不可變對象copy是淺拷貝
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
}
return 0;
}
@property進階:深入理解
由于篇幅有限,本文只用于介紹property基本用法,博客另一篇文章會深入講解property的實現(xiàn)機制,有興趣可自行查閱iOS @property探究(二): 深入理解
備注
由于作者水平有限,難免出現(xiàn)紕漏,如有問題還請不吝賜教。