二、對(duì)象吐辙、消息宣决、運(yùn)行期(1)
- 『對(duì)象 object』是使用面向?qū)ο笳Z(yǔ)言編程時(shí)的『基本構(gòu)造單元』。
- 開(kāi)發(fā)者可以通過(guò)對(duì)象存儲(chǔ)昏苏、傳遞數(shù)據(jù)尊沸,對(duì)象之間傳遞數(shù)據(jù)并執(zhí)行任務(wù)的過(guò)程成為『消息傳遞 Messaging』。
- Objective-C Runtime 是在程序運(yùn)行期間提供相關(guān)支持的代碼贤惯,它提供了使得對(duì)象之間能夠發(fā)送消息的重要函數(shù)洼专,并且包含類對(duì)象創(chuàng)建的全部邏輯。
6救巷、理解『屬性』這一概念
-
『屬性 property』是 OC 用于封裝對(duì)象中的數(shù)據(jù)而提供的一個(gè)特性壶熏。OC 對(duì)象通常會(huì)把其需的數(shù)據(jù)保存為各種實(shí)例變量。實(shí)例變量一般通過(guò)『存取方法』來(lái)訪問(wèn)浦译。
getter
方法用于讀取棒假,setter
方法用于寫(xiě)入精盅。 - 在 OC 2.0 中引入屬性這一特性叹俏,開(kāi)發(fā)者可令編譯器自動(dòng)生成屬性的存取方法屡谐。此特性還引入了『點(diǎn)語(yǔ)法』是開(kāi)發(fā)者更容易訪問(wèn)對(duì)象中的數(shù)據(jù)愕掏。
- 類似 Java 和C++ 的寫(xiě)法剑梳,OC可以這樣定義一個(gè)類:
@interface EOCPerson : NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end;
這種寫(xiě)法的問(wèn)題是:對(duì)象布局在編譯器就已經(jīng)固定了垢乙。只要是訪問(wèn)_firstName變量的代碼追逮,編譯器就把其替換為『偏移量 offset』羊壹,這個(gè)偏移量是『硬編碼』,表示該變量距離存放對(duì)象的內(nèi)存區(qū)域的起始位置有多遠(yuǎn)稠茂。這樣的話睬关,如果在_firstName前又添加一個(gè)變量NSDate *_dateOfBirth;
电爹,那么原來(lái)_firstName的偏移量現(xiàn)在會(huì)指向_dateOfBirth丐箩,會(huì)讀取到錯(cuò)誤的值屎勘。所以這種寫(xiě)法在修改了類定義之后必須重新編譯概漱,以確保編譯器計(jì)算正確的偏移量,倘若兩份代碼一份使用舊的類定義玉吁,一份使用新的诈茧,就會(huì)出現(xiàn)不兼容的現(xiàn)象敢会。
- OC 的解決辦法是鸥昏,把實(shí)例變量當(dāng)做一種存儲(chǔ)偏移量的特殊變量吏垮,交由『類對(duì)象』管理膳汪,偏移量會(huì)在運(yùn)行期查找,如果類定義變了鼓蜒,偏移量也會(huì)變化都弹。這樣不僅可以確保偏移量的正確性冯痢,還可以在運(yùn)行期向類中添加實(shí)例變量浦楣,這就是穩(wěn)固的『應(yīng)用程序二進(jìn)制接口 ABI』椒振。得益于穩(wěn)固的 ABI澎迎,我們可以在類的 class-continuation 分類(即空名的分類)或?qū)崿F(xiàn)文件中定義實(shí)例變量夹供。
- 另一個(gè)解決辦法哮洽,就是不要直接訪問(wèn)實(shí)例變量,而是通過(guò)存取方法來(lái)訪問(wèn)氛什,這時(shí)『屬性』就派上用場(chǎng)了枪眉。通過(guò)屬性贸铜,可以訪問(wèn)封裝在對(duì)象里的數(shù)據(jù)。所以可以把屬性當(dāng)做是編譯器的一系列操作的簡(jiǎn)稱蛋济,因?yàn)槭褂昧藢傩赃@一特性后鹊杖,編譯器會(huì)自動(dòng)生成一套存取方法以及對(duì)應(yīng)給定名稱的實(shí)例變量。
// 通過(guò)屬性定義類的的實(shí)例變量
@interface EOCPerson : NSObject {
@property NSString *firstName;
@property NSString *lastName;
}
@end;
// 對(duì)于使用者來(lái)說(shuō), 以上代碼等同于:
@interface EOCPerson : NSObject {
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
}
@end;
- 使用屬性的優(yōu)勢(shì):
- 可以使用點(diǎn)語(yǔ)法訪問(wèn)屬性川尖,和使用存取方法沒(méi)有差別。
- 編譯器在編譯期會(huì)自動(dòng)編寫(xiě)訪問(wèn)屬性的存取方法馍悟,此過(guò)程叫『自動(dòng)合成』锣咒。
- 編譯器在編譯期會(huì)自動(dòng)向類中添加適當(dāng)類型的實(shí)例變量趣兄,實(shí)例變量的名稱為屬性名前加下劃線艇潭。
- 我們可以使用
@synthesize
語(yǔ)法來(lái)自己指定實(shí)例變量的名稱蹋凝。使用@dynamic
語(yǔ)法指定屬性不自動(dòng)生成存取方法和實(shí)例變量仙粱,即使編譯期時(shí)編譯器沒(méi)有發(fā)現(xiàn)存取方法也不會(huì)報(bào)錯(cuò),因?yàn)樗J(rèn)為這些方法在運(yùn)行期能找到刃唤。
-
屬性特質(zhì):屬性的不同特質(zhì)會(huì)影響編譯器所生成的存取方法硬霍。屬性的特質(zhì)可以分四類:
-
原子性:默認(rèn)情況下,由編譯器所生成的存取方法會(huì)通過(guò)添加同步鎖的機(jī)制確保屬性的原子性
atomic
(在多線程中同一時(shí)間只能有一個(gè)線程執(zhí)行屬性的存取操作)拜轨。 如果屬性具備nonatomic
橄碾,則不使用同步鎖。 -
讀/寫(xiě)權(quán)限:
-
readwrite 屬性擁有
setter
和getter
方法; -
readonly 屬性只有
getter
拒垃〉课停可以使用此特質(zhì)把某個(gè)屬性對(duì)外公開(kāi)為只讀屬性谤牡,然后在class-continuation
分類中將其重新定義為讀寫(xiě)屬性副硅。
-
readwrite 屬性擁有
-
內(nèi)存管理語(yǔ)義: 屬性用于封裝數(shù)據(jù),而數(shù)據(jù)應(yīng)具有『具體所有權(quán)語(yǔ)義』翅萤。下面一組特質(zhì)只會(huì)影響
setter
方法恐疲。如果是自己編寫(xiě)的setter
方法,則必須與屬性具備的特質(zhì)相符套么。-
assign
setter
方法只會(huì)執(zhí)行針對(duì)『純量類型』(scalar type 如 CGFloat培己, NSInteger 等基本數(shù)據(jù)類型)的簡(jiǎn)單賦值操作。 -
strong 表明該屬性定義了一種『擁有關(guān)系』胚泌,為該屬性設(shè)置新值時(shí)省咨,
setter
方法會(huì)先保留新值玷室,并釋放舊值津肛,然后將新值設(shè)置上去。 -
weak 表明該屬性定義了一種『非擁有關(guān)系』搪花,為該屬性設(shè)置新值時(shí),
setter
方法既不會(huì)保留新值僚匆,也不會(huì)釋放舊值松申,和 assign 類似,只是簡(jiǎn)單的賦值彪置。但是在該屬性所指對(duì)象被銷毀時(shí)耀盗,屬性值也會(huì)被清空忿薇。 - unsafe_unretained 和 assign 類似筋栋,但是它適用于『對(duì)象類型』,和 weak 類似,也是定義一種『非擁有關(guān)系(unretained)』徒欣,但是當(dāng)目標(biāo)對(duì)象被銷毀時(shí),屬性值不會(huì)自動(dòng)清空(unsafe)奏纪。
-
copy 和 strong 類似,但是
setter
方法不會(huì)保留新值墩朦,而是將其拷貝。多用在 NSString* 類型凰荚,來(lái)保護(hù)其封裝性。因?yàn)閭鹘osetter
方法的新值可能一個(gè) NSMutableString 類型的可變字符串屿讽,其值可能會(huì)被修改柱徙。所以拷貝一份不可變的字符串羊初,可以確保屬性對(duì)象的值不會(huì)遭人修改得哆。一般來(lái)說(shuō),只要屬性所用的對(duì)象是可變的哈街,就應(yīng)該在setter
方法中對(duì)其進(jìn)行拷貝。
-
assign
-
方法名: 可以指定存取方法的方法名
- getter=<name> 如 UISwitch 類的的 on 屬性
-
原子性:默認(rèn)情況下,由編譯器所生成的存取方法會(huì)通過(guò)添加同步鎖的機(jī)制確保屬性的原子性
@property (nonatomic, getter=isOn) BOOL on;
// 使用時(shí)
switch.isOn;
- **setter=<name>** 不常見(jiàn)屹电。
- 在我們?yōu)轭愖远x了一個(gè)初始化方法時(shí)阶剑,也要遵循屬性的特質(zhì)來(lái)為其賦值,如我們?yōu)轭惗x了一個(gè) 具有copy 特質(zhì)字符串危号,又定義了一個(gè)用該字符串初始化一個(gè)類的實(shí)例的方法牧愁。
@property (copy) NSString *name;
- (id)initWithName:(NSString *)name;
初始化方法的實(shí)現(xiàn)可以這樣寫(xiě):
- (id)initWithName:(NSString *)name
{
if(self = [super init]) {
_name = [name copy];
}
return self;
}
為什么這里不直接調(diào)用setter
方法呢?第7條會(huì)做詳細(xì)解釋。
- 如果使用了
readonly
外莲,編譯器只會(huì)創(chuàng)建getter
猪半,即便如此,還是要寫(xiě)上內(nèi)存管理語(yǔ)義,以表明在初始化方法中設(shè)置這些屬性所用的方式磨确。否則使用者可能會(huì)在初始化之后自行拷貝沽甥,這種操作是多余而且低效的。 - 使用
atomic
的好處是俐填,如果有兩個(gè)線程讀寫(xiě)同一屬性安接,那么不論何時(shí),各個(gè)線程中總能得到有效的屬性值英融。如果使用nonaotmic
,其中一個(gè)線程讀寫(xiě)時(shí)歇式,另一個(gè)線程突然闖入讀取或修改了尚未修改好的值驶悟,最后得到的值可能會(huì)不對(duì)。但是為什么通常開(kāi)發(fā)中都是用nonatomic
材失,原因是痕鳍,在 iOS 中使用同步鎖的開(kāi)銷較大,會(huì)帶來(lái)性能問(wèn)題龙巨。此外笼呆,就算使用了atomic
也不一定能保證線程安全。例如旨别,一個(gè)線程連續(xù)多次讀取某個(gè)屬性的值的過(guò)程中诗赌,有別的線程在同時(shí)改寫(xiě)該值,那么最后得到的值還是會(huì)可能出錯(cuò)秸弛。要保證線程安全铭若,需要采用更深層的鎖定機(jī)制。不過(guò)在 macOS 中递览,使用atomic
不會(huì)有性能問(wèn)題叼屠。
7、在對(duì)象內(nèi)部盡量直接訪問(wèn)實(shí)例變量
- 直接訪問(wèn)和通過(guò)屬性訪問(wèn)的區(qū)別是:
- 直接訪問(wèn)不經(jīng)過(guò) OC 的『方法派發(fā)』(見(jiàn)11條)绞铃,速度會(huì)比較快镜雨,相當(dāng)于直接訪問(wèn)保存對(duì)象實(shí)例變量的內(nèi)存。
- 直接訪問(wèn)不會(huì)調(diào)用
setter
方法儿捧,這會(huì)繞過(guò)為屬性定義的『內(nèi)存管理語(yǔ)義』荚坞,如在 ARC 下直接訪問(wèn)一個(gè)copy
屬性,并不會(huì)拷貝該屬性纯命。 - 直接訪問(wèn)不會(huì)觸發(fā)『鍵值觀察 KVO』通知西剥。
- 通過(guò)屬性訪問(wèn)可以給存取方法設(shè)置斷點(diǎn),這有助于排插錯(cuò)誤亿汞。
- 作者建議在讀取實(shí)例變量時(shí)直接訪問(wèn)瞭空,而在設(shè)置時(shí)通過(guò)屬性來(lái)做,以盡可能提高效率。這種做法需要注意的問(wèn)題:
- 在初始化方法中應(yīng)該總是采用直接訪問(wèn)咆畏,因?yàn)樽宇惪赡軙?huì)重寫(xiě)
setter
方法南捂。但是,如果待初始化的實(shí)例變量聲明在超類中旧找,我們無(wú)法在子類中直接訪問(wèn)此實(shí)例變量溺健,就只能調(diào)用setter
方法來(lái)初始化。 - 如果某個(gè)屬性采用了懶加載钮蛛,則必須通過(guò)存取方法訪問(wèn)屬性鞭缭。否則,實(shí)例變量永遠(yuǎn)不會(huì)被初始化魏颓。
- 在初始化方法中應(yīng)該總是采用直接訪問(wèn)咆畏,因?yàn)樽宇惪赡軙?huì)重寫(xiě)
8岭辣、理解『對(duì)象等同性』這一概念
- 使用 『==』操作符比較的是兩個(gè)指針本身是否相同,或者說(shuō)內(nèi)存地址是否相同甸饱。
- 比較兩個(gè)對(duì)象的同等性應(yīng)當(dāng)使用 NSObject 協(xié)議中聲明的
isEqual:
方法沦童。 - 有的類提供了專門(mén)的比較方法,如 NSString 的
isEqualToString:
方法叹话,應(yīng)當(dāng)優(yōu)先使用偷遗,這比調(diào)用isEqual
方法快,因?yàn)?code>isEqual需要執(zhí)行額外的步驟來(lái)判斷比較對(duì)象的類型驼壶。 - NSObject 協(xié)議中用于判斷等同性的關(guān)鍵方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
這兩個(gè)方法的默認(rèn)實(shí)現(xiàn)是:當(dāng)且僅當(dāng)其『指針值(或內(nèi)存地址)』完全相等時(shí)氏豌,這兩個(gè)對(duì)象才相等。若要在自定義對(duì)象中重寫(xiě)這兩個(gè)方法辅柴,則必須保證:如果isEqual
方法判斷兩個(gè)對(duì)象相等箩溃,則 hash
方法也必須返回相同的值。但是如果hash
返回相同的值碌嘀,那么isEqual
方法未必認(rèn)為兩個(gè)對(duì)象相等涣旨。一種較好的計(jì)算 hash 值的方式:
// 以前面提到的 EOCPerson 類為例:
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
-
hash
方法返回的值對(duì) Set 集合的性能有很大的影響,因?yàn)?Set 在檢索哈希表時(shí)股冗,會(huì)用對(duì)象的哈希值做索引霹陡。Set 可能會(huì)根據(jù)哈希值把對(duì)象分裝到不同的數(shù)組,想 Set 中添加對(duì)象時(shí)止状,要根據(jù)其哈希值找到對(duì)應(yīng)的數(shù)組烹棉,依次檢查其中的各個(gè)元素,判斷是否和新對(duì)象相等怯疤。所以浆洗,如果每個(gè)對(duì)象返回的 hash 值相同吸占,那么 Set 需要將所有對(duì)象掃描一邊辫愉,會(huì)影響性能。 - 特定的類有特定的等同性判斷方法叶堆,如系統(tǒng)為 NSString、NSArray摘昌、NSDictionary 等提供了特別的判斷方法速妖,但由于 OC 在編譯期不做強(qiáng)類型檢查,傳入這些方法的參數(shù)類型需要保證正確聪黎,否則會(huì)拋出異常罕容。可以為自定義類編寫(xiě)類似的方法稿饰,并一并復(fù)寫(xiě)
isEqual:
方法锦秒,后者常見(jiàn)的實(shí)現(xiàn)方法是如果傳入的參數(shù)類型正確就使用自己編寫(xiě)的判斷方法,否則交由超類比較湘纵。 - 等同性的執(zhí)行深度:在我們?yōu)樽远x類實(shí)現(xiàn)判斷方法時(shí)脂崔,可以根據(jù)不同需求針對(duì)部分或全部的字段做比較。如從數(shù)據(jù)庫(kù)讀取的數(shù)據(jù)可以只比較主鍵 id 是否相同梧喷。
- 容器中如果存在可變類,可能會(huì)影響容器等同性的結(jié)果脖咐。例如铺敌,在 Set 里添加一個(gè)可變數(shù)組,再添加一個(gè)與之不同的可變數(shù)組屁擅,接著修改第二個(gè)可變數(shù)組同第一個(gè)相等偿凭,此時(shí) Set 里依然有兩個(gè)完全相同的數(shù)組,這與 Set 的語(yǔ)義相違背派歌。并且弯囊,如果此時(shí)復(fù)制一份該 Set,得到的結(jié)果是只有一個(gè)數(shù)組的 Set胶果。所以匾嘱,要么保證添加到 Set 里的對(duì)象的 hash 值不是根據(jù)可變字段計(jì)算的,要么就保證這個(gè)對(duì)象是不可變的早抠。
9霎烙、以『類族模式』隱藏實(shí)現(xiàn)細(xì)節(jié)
- 使用『類族』模式,可以隱藏『抽象基類』的實(shí)現(xiàn)細(xì)節(jié)蕊连。OC 的系統(tǒng)框架中普遍使用了這種模式悬垃,如 UIButton,其提供的工廠方法
buttonWithType:
可以根據(jù)傳入的參數(shù)生成不同類型的 button甘苍,但是這寫(xiě)不同類型的 button 都繼承自基類 UIButton尝蠕。 - 由工廠方法返回的實(shí)例對(duì)象并不是一定是基類的實(shí)例對(duì)象,所以
[button isMemberOfClass:[UIButton class]];
返回的是 NO载庭。 - 除了 UIButton看彼,大部分的集合類都是類族廊佩,通過(guò) class 返回的類型便可知道,如一個(gè) NSArray 對(duì)象執(zhí)行
[array class];
返回的類可能是__NSArrayI
闲昭、__NSArrayM
罐寨、__NSArray0
等,絕不可能返回NSArray
類本身序矩。所以使用如下的代碼是錯(cuò)誤的鸯绿。
if ([maybeAnArray class] == [NSArray class]) {
// do someting
}
應(yīng)當(dāng)使用類型信息查詢方法
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
// do someting
}
- 編寫(xiě)類族的子類時(shí),有以下幾條規(guī)則:
- 子類應(yīng)該繼承自類族中的抽象基類簸淀。
- 子類應(yīng)該定義自己的數(shù)據(jù)存儲(chǔ)方式瓶蝴。因?yàn)橄?NSArry 本身只不過(guò)是包在其他隱藏對(duì)象外面的殼,僅僅定義了所有數(shù)據(jù)都需要具備的一些接口租幕,并沒(méi)有定義數(shù)據(jù)的存儲(chǔ)方式舷手。對(duì)于自定義的數(shù)組來(lái)說(shuō),可以用 NSArray 來(lái)保存它的實(shí)例劲绪。
- 子類應(yīng)當(dāng)覆寫(xiě)超類文檔中明確指明需要覆寫(xiě)的方法男窟。如 NSArray 的子類必須覆寫(xiě)
- count
和- objectAtIndex:
方法。
10贾富、在既有類中使用關(guān)聯(lián)對(duì)象存放自定義數(shù)據(jù)
- 同過(guò)使用『關(guān)聯(lián)對(duì)象』的特性歉眷,可以在某個(gè)對(duì)象中存儲(chǔ)一些額外的數(shù)據(jù)〔梗可以給某個(gè)對(duì)象關(guān)聯(lián)多個(gè)其他對(duì)象汗捡,這些對(duì)象通過(guò)『鍵』來(lái)區(qū)分。關(guān)聯(lián)時(shí)可以指明對(duì)象的內(nèi)存管理語(yǔ)義畏纲。
關(guān)聯(lián)類型 | 等效@property中的 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic扇住,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
下列方法可以管理關(guān)聯(lián)對(duì)象:
// 設(shè)置對(duì)象value為object 的鍵為*key, 存儲(chǔ)策略為 policy 的關(guān)聯(lián)對(duì)象
- void objc_setAssociatedObject (id object, void *key, id value, objc_AssociatedPolicy policy)
// 獲取object對(duì)象中的鍵為 *key 的關(guān)聯(lián)對(duì)象值.
- id objc_getAssociatedObject (id object, void *key)
// 移除 object 的所有關(guān)聯(lián)對(duì)象
- void objc_removeAssociatedObject(id object)
- 只有在其他辦法不可行時(shí)才去使用關(guān)聯(lián)對(duì)象盗胀,因?yàn)槿绻麨E用會(huì)令代碼失控艘蹋,難于調(diào)試。