iOS @property探究(一): 基礎(chǔ)詳解

你要知道的@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)建變量的gettersetter的實現(xiàn),gettersetter本質(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)用方式可以看出,settergetter本質(zhì)就是實例方法,可以通過函數(shù)調(diào)用的方式來使用。
為了方便使用,Objective-C允許使用點語法來訪問gettersetter。

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)建的settergetter。
當有很多變量需要設(shè)置時,這樣手工創(chuàng)建settergetter的方式難免很繁瑣,因此合成存取方法就誕生了。

合成存取方法

@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ù)類型,如整形使用NSIntegerNSUInteger表示,時間間隔的浮點類型使用NSTimeInterval表示,這樣代碼數(shù)據(jù)類型更統(tǒng)一。

上面的代碼使用@property聲明兩個屬性nameage并為其設(shè)置了一些指示符(nonatomic,copy,assign等,下文會詳細介紹)。
@synthesize表示為這兩個屬性自動生成名為_name_age的底層實例變量,并自動生成相關(guān)的gettersetter也可以不寫編譯器默認會自動生成'_屬性名'的實例變量以及相關(guān)的gettersetter。
這里所說的編譯器自動生成的實例變量就如同我們在上文中手動創(chuàng)建settergetter時聲明的變量_name_age。也就是說編譯器會在編譯時會自動生成并使用_name_age這兩個變量來存儲這兩個屬性,跟nameage沒什么關(guān)系了,只是我們在上層使用這兩個屬性的時候可以用nameage的點語法來訪問gettersetter。如果不想使用這兩個名字用于底層的存儲也可以任意命名,但最好按照官方的命名原則來命名。

也可以自定義getter和setter方法來覆蓋編譯器默認生成的方法,就如同手動創(chuàng)建gettersetter一樣。

@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 nonatomic
  • readwrite readonly
  • assign
  • strong
  • weak
  • copy
  • unsafe_unretained
  • retain

還可以設(shè)置gettersetter對其重命名,這里不再贅述。

atomic/nonatomic

指定合成存取方法是否為原子操作,可以理解為是否線程安全,但在iOS上即時使用atomic也不一定是線程安全的,要保證線程安全需要使用鎖機制,超過本文的講解范圍,可以自行查閱。
可以發(fā)現(xiàn)幾乎所有代碼的屬性設(shè)置都會使用nonatomic,這樣能夠提高訪問性能,在iOS中使用鎖機制的開銷較大,會損耗性能。

readwrite/readonly

readwrite是編譯器的默認選項,表示自動生成gettersetter,如果需要gettersetter不寫即可。
readonly表示只合成getter而不合成setter。

assign、weak、unsafe_unretained

assign表示對屬性只進行簡單的賦值操作,不更改所賦的新值的引用計數(shù),也不改變舊值的引用計數(shù),常用于標量類型,如NSIntegerNSUInteger,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_unretainedassign的區(qū)別在于,unsafe_unretained只能修飾對象,不能修飾標量類型,而assign兩者均可修飾。

為了防止多態(tài)的影響,對NSString進行修飾時一般使用copy。

下文會對weak、unsafe_unretainedcopy進行詳細介紹。

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修飾,使用strongassign等修飾則會因為多態(tài)導致屬性值被修改。
這里的copy還牽扯到NSCopyingNSMutableCopying協(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探究(二): 深入理解

對于可變對象類型,如NSMutableStringNSMutableArray等則不可以使用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_unretainedassign的區(qū)別在于,unsafe_unretained只能修飾對象,不能修飾標量類型,而assign兩者均可修飾。

retain

在ARC環(huán)境下使用較少,在MRC下使用效果與strong一致。

copy的題外話

有時候我們需要copy一個對象,或是mutableCopy一個對象,這時需要遵守NSCopyingNSMutableCopying協(xié)議,來實現(xiàn)copyWithZone:mutableCopyWithZone:兩個方法,而不是重寫copymutableCopy兩個方法。
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)紕漏,如有問題還請不吝賜教。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容