二、對(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ù),并且包含類(lèi)對(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ù)。
- 類(lèi)似 Java 和C++ 的寫(xiě)法,OC可以這樣定義一個(gè)類(lèi):
@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)。這樣的話(huà),如果在_firstName前又添加一個(gè)變量NSDate *_dateOfBirth;,那么原來(lái)_firstName的偏移量現(xiàn)在會(huì)指向_dateOfBirth,會(huì)讀取到錯(cuò)誤的值。所以這種寫(xiě)法在修改了類(lèi)定義之后必須重新編譯,以確保編譯器計(jì)算正確的偏移量,倘若兩份代碼一份使用舊的類(lèi)定義,一份使用新的,就會(huì)出現(xiàn)不兼容的現(xiàn)象。
- OC 的解決辦法是,把實(shí)例變量當(dāng)做一種存儲(chǔ)偏移量的特殊變量,交由『類(lèi)對(duì)象』管理,偏移量會(huì)在運(yùn)行期查找,如果類(lèi)定義變了,偏移量也會(huì)變化。這樣不僅可以確保偏移量的正確性,還可以在運(yùn)行期向類(lèi)中添加實(shí)例變量,這就是穩(wěn)固的『應(yīng)用程序二進(jìn)制接口 ABI』。得益于穩(wěn)固的 ABI,我們可以在類(lèi)的 class-continuation 分類(lèi)(即空名的分類(lèi))或?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)稱(chēng),因?yàn)槭褂昧藢傩赃@一特性后,編譯器會(huì)自動(dòng)生成一套存取方法以及對(duì)應(yīng)給定名稱(chēng)的實(shí)例變量。
// 通過(guò)屬性定義類(lèi)的的實(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)向類(lèi)中添加適當(dāng)類(lèi)型的實(shí)例變量,實(shí)例變量的名稱(chēng)為屬性名前加下劃線(xiàn)。
- 我們可以使用
@synthesize語(yǔ)法來(lái)自己指定實(shí)例變量的名稱(chēng)。使用@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ì)可以分四類(lèi):
-
原子性:默認(rèn)情況下,由編譯器所生成的存取方法會(huì)通過(guò)添加同步鎖的機(jī)制確保屬性的原子性
atomic(在多線(xiàn)程中同一時(shí)間只能有一個(gè)線(xiàn)程執(zhí)行屬性的存取操作)。 如果屬性具備nonatomic,則不使用同步鎖。 -
讀/寫(xiě)權(quán)限:
-
readwrite 屬性擁有
setter和getter方法; -
readonly 屬性只有
getter??梢允褂么颂刭|(zhì)把某個(gè)屬性對(duì)外公開(kāi)為只讀屬性,然后在class-continuation分類(lèi)中將其重新定義為讀寫(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ì)『純量類(lèi)型』(scalar type 如 CGFloat, NSInteger 等基本數(shù)據(jù)類(lèi)型)的簡(jiǎn)單賦值操作。 -
strong 表明該屬性定義了一種『擁有關(guān)系』,為該屬性設(shè)置新值時(shí),
setter方法會(huì)先保留新值,并釋放舊值,然后將新值設(shè)置上去。 -
weak 表明該屬性定義了一種『非擁有關(guān)系』,為該屬性設(shè)置新值時(shí),
setter方法既不會(huì)保留新值,也不會(huì)釋放舊值,和 assign 類(lèi)似,只是簡(jiǎn)單的賦值。但是在該屬性所指對(duì)象被銷(xiāo)毀時(shí),屬性值也會(huì)被清空。 - unsafe_unretained 和 assign 類(lèi)似,但是它適用于『對(duì)象類(lèi)型』,和 weak 類(lèi)似,也是定義一種『非擁有關(guān)系(unretained)』,但是當(dāng)目標(biāo)對(duì)象被銷(xiāo)毀時(shí),屬性值不會(huì)自動(dòng)清空(unsafe)。
-
copy 和 strong 類(lèi)似,但是
setter方法不會(huì)保留新值,而是將其拷貝。多用在 NSString* 類(lèi)型,來(lái)保護(hù)其封裝性。因?yàn)閭鹘osetter方法的新值可能一個(gè) NSMutableString 類(lèi)型的可變字符串,其值可能會(huì)被修改。所以拷貝一份不可變的字符串,可以確保屬性對(duì)象的值不會(huì)遭人修改。一般來(lái)說(shuō),只要屬性所用的對(duì)象是可變的,就應(yīng)該在setter方法中對(duì)其進(jìn)行拷貝。
-
assign
-
方法名: 可以指定存取方法的方法名
- getter=<name> 如 UISwitch 類(lèi)的的 on 屬性
-
原子性:默認(rèn)情況下,由編譯器所生成的存取方法會(huì)通過(guò)添加同步鎖的機(jī)制確保屬性的原子性
@property (nonatomic, getter=isOn) BOOL on;
// 使用時(shí)
switch.isOn;
- **setter=<name>** 不常見(jiàn)。
- 在我們?yōu)轭?lèi)自定義了一個(gè)初始化方法時(shí),也要遵循屬性的特質(zhì)來(lái)為其賦值,如我們?yōu)轭?lèi)定義了一個(gè) 具有copy 特質(zhì)字符串,又定義了一個(gè)用該字符串初始化一個(gè)類(lèi)的實(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àn)程讀寫(xiě)同一屬性,那么不論何時(shí),各個(gè)線(xiàn)程中總能得到有效的屬性值。如果使用nonaotmic,其中一個(gè)線(xiàn)程讀寫(xiě)時(shí),另一個(gè)線(xiàn)程突然闖入讀取或修改了尚未修改好的值,最后得到的值可能會(huì)不對(duì)。但是為什么通常開(kāi)發(fā)中都是用nonatomic,原因是,在 iOS 中使用同步鎖的開(kāi)銷(xiāo)較大,會(huì)帶來(lái)性能問(wèn)題。此外,就算使用了atomic也不一定能保證線(xiàn)程安全。例如,一個(gè)線(xiàn)程連續(xù)多次讀取某個(gè)屬性的值的過(guò)程中,有別的線(xiàn)程在同時(shí)改寫(xiě)該值,那么最后得到的值還是會(huì)可能出錯(cuò)。要保證線(xiàn)程安全,需要采用更深層的鎖定機(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)樽宇?lèi)可能會(huì)重寫(xiě)
setter方法。但是,如果待初始化的實(shí)例變量聲明在超類(lèi)中,我們無(wú)法在子類(lèi)中直接訪問(wèn)此實(shí)例變量,就只能調(diào)用setter方法來(lái)初始化。 - 如果某個(gè)屬性采用了懶加載,則必須通過(guò)存取方法訪問(wèn)屬性。否則,實(shí)例變量永遠(yuǎn)不會(huì)被初始化。
- 在初始化方法中應(yīng)該總是采用直接訪問(wèn),因?yàn)樽宇?lèi)可能會(huì)重寫(xiě)
8、理解『對(duì)象等同性』這一概念
- 使用 『==』操作符比較的是兩個(gè)指針本身是否相同,或者說(shuō)內(nèi)存地址是否相同。
- 比較兩個(gè)對(duì)象的同等性應(yīng)當(dāng)使用 NSObject 協(xié)議中聲明的
isEqual:方法。 - 有的類(lèi)提供了專(zhuān)門(mén)的比較方法,如 NSString 的
isEqualToString:方法,應(yīng)當(dāng)優(yōu)先使用,這比調(diào)用isEqual方法快,因?yàn)?code>isEqual需要執(zhí)行額外的步驟來(lái)判斷比較對(duì)象的類(lèi)型。 - 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 類(lèi)為例:
- (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ì)影響性能。 - 特定的類(lèi)有特定的等同性判斷方法,如系統(tǒng)為 NSString、NSArray、NSDictionary 等提供了特別的判斷方法,但由于 OC 在編譯期不做強(qiáng)類(lèi)型檢查,傳入這些方法的參數(shù)類(lèi)型需要保證正確,否則會(huì)拋出異常??梢詾樽远x類(lèi)編寫(xiě)類(lèi)似的方法,并一并復(fù)寫(xiě)
isEqual:方法,后者常見(jiàn)的實(shí)現(xiàn)方法是如果傳入的參數(shù)類(lèi)型正確就使用自己編寫(xiě)的判斷方法,否則交由超類(lèi)比較。 - 等同性的執(zhí)行深度:在我們?yōu)樽远x類(lèi)實(shí)現(xiàn)判斷方法時(shí),可以根據(jù)不同需求針對(duì)部分或全部的字段做比較。如從數(shù)據(jù)庫(kù)讀取的數(shù)據(jù)可以只比較主鍵 id 是否相同。
- 容器中如果存在可變類(lèi),可能會(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、以『類(lèi)族模式』隱藏實(shí)現(xiàn)細(xì)節(jié)
- 使用『類(lèi)族』模式,可以隱藏『抽象基類(lèi)』的實(shí)現(xiàn)細(xì)節(jié)。OC 的系統(tǒng)框架中普遍使用了這種模式,如 UIButton,其提供的工廠方法
buttonWithType:可以根據(jù)傳入的參數(shù)生成不同類(lèi)型的 button,但是這寫(xiě)不同類(lèi)型的 button 都繼承自基類(lèi) UIButton。 - 由工廠方法返回的實(shí)例對(duì)象并不是一定是基類(lèi)的實(shí)例對(duì)象,所以
[button isMemberOfClass:[UIButton class]];返回的是 NO。 - 除了 UIButton,大部分的集合類(lèi)都是類(lèi)族,通過(guò) class 返回的類(lèi)型便可知道,如一個(gè) NSArray 對(duì)象執(zhí)行
[array class];返回的類(lèi)可能是__NSArrayI、__NSArrayM、__NSArray0等,絕不可能返回NSArray類(lèi)本身。所以使用如下的代碼是錯(cuò)誤的。
if ([maybeAnArray class] == [NSArray class]) {
// do someting
}
應(yīng)當(dāng)使用類(lèi)型信息查詢(xún)方法
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
// do someting
}
- 編寫(xiě)類(lèi)族的子類(lèi)時(shí),有以下幾條規(guī)則:
- 子類(lèi)應(yīng)該繼承自類(lèi)族中的抽象基類(lèi)。
- 子類(lèi)應(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í)例。
- 子類(lèi)應(yīng)當(dāng)覆寫(xiě)超類(lèi)文檔中明確指明需要覆寫(xiě)的方法。如 NSArray 的子類(lèi)必須覆寫(xiě)
- count和- objectAtIndex:方法。
10、在既有類(lèi)中使用關(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)類(lèi)型 | 等效@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)試。