Effective Objective-C 2.0 總結與筆記(第二章)—— 物件、訊息、執行期
第二章:物件、訊息、執行期
“物件”就是“基本構造單元”,開發者可以通過物件來儲存並傳遞資料。物件之間傳遞資料並執行任務的過程就是“訊息傳遞”。程式執行起來後,為其提供相關支援的程式碼就是“Objective-C執行期環境”,它提供了一些使得物件之間能夠傳遞訊息的重要函式,並且包括建立類例項所用的全部邏輯。
第6條:理解“屬性”這一概念
- ”屬性“ (property) 是Objective-C的一項特性,用於封裝物件中的資料。例項變數一般通過“存取方法”來訪問,其中getter用於讀取變數值,setter用於寫入變數值。開發者可以令編譯器自動編寫與屬性有關的存取方法。
- Objective-C語言很少將例項變數放在類介面的public區段內,更多的是使用
@property
的方式來宣告。這是因為如果直接使用public的區段,當例項變數的偏移量改變(原有的例項變數裡插入了新的例項變數)的時候,需要重新編譯,假如程式碼庫中某份程式碼使用了舊的類定義,就會出現不相容的情況。
//public區段內宣告例項變數 @interface EOCPerson : NSObject { @public NSData *_dataOfBirth; NSString *_firstName; NSString *_lastName; } //使用@property方式宣告 @interface EOCPerson : NSObject @property(nonatomic, copy) NSString *firstName; @property(nonatomic, copy) NSString *lastName; @end //@property方式等效於 @interface EOCPerson : NSObject - (NSString *)firstName; - (void)setFirstName; - (NSString *)lastName; - (void)setLastName; @end
如果要訪問屬性,可以使用“點語法”,編譯器會把“點語法”轉換成對存取方法的呼叫。
EOCPerson *aPerson = [EOCPerson new];
aPerson.firstName = @"GDGD";//same as
[aPerson setFirstName:@"GDGD"];
NSString *lastNameOfGD = aPerson.lastName;//same as
NSString *lastNameOfGD = [aPerson lastName];
使用屬性的方式進行宣告例項變數,編譯器會自動生成存取方法,雖然這個時候在編譯器上看不到生成的方法,這個方法預設的名字是:getter—就是屬性的名字,setter—屬性名字前面加set。
如果想要修改“合成方法”的名字,可以使用@synthesize語法來指定關鍵字的名字。
//在實現檔案中
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
如果不想要編譯器自動生成的方法,可以使用@dynamic語法來阻止編譯器的生成。
@implementation EOCPerson
@dynamic firstName, lastName;
@end
- 屬性具有四種特質,不同的特質也會影響屬性生成的存取方法。
(1)、原子性:預設情況下,編譯器所合成的方法會通過鎖定機制確保其原子性 (atomicity) 。如果屬性具備nonatomic特質,則不使用同步鎖。在iOS開發的程式裡,由於同步鎖開銷較大,如果使用原子性的屬性會導致效能問題,所以屬性都是nonatomic的特質。
(2)、讀寫許可權:
readwrite (讀寫) :擁有前面說的獲取方法 (getter) 和設定方法 (setter),預設情況。
readonly (只讀) :僅擁有獲取方法。可以對外暴露為只讀屬性,然後在“class-continuation分類”中重新定義為讀寫屬性。(27條有說)
(3)、記憶體管理語義:這是最難理解的部分了,雖然這個特質僅會影響“設定方法”,但是其中涉及到記憶體管理部分,所以還是非常重要。
assign:“設定方法”只會針對“純量型別”(scalar type,例如CGFloat,NSInteger等)的簡單賦值。一般在之前分配在棧裡的資料型別就是使用assign修飾。
strong:此特質表示一種擁有關係,當給這種屬性設定新值的時候,會先保留新值,再釋放舊值,然後設定新值上去,一般Objective-C物件使用這個修飾詞。
weak:表示非擁有關係,為這種屬性設定新值的時候,不保留新值,也不釋放舊值,當屬性所指的物件被銷燬的時候,屬性值也會被清空。這就是一種弱持有,一般可以用來打破迴圈引用的情況(後面會介紹)。
unsafe_unretained :語義和assign相同,但是適用於物件型別(object type),表示一種非擁有關係,但是當目標物件被銷燬的時候,屬性值不會被清空。
copy:和strong類似,但是設定方法並不保留新值,而是將其拷貝。一般NSString *就使用這個特質來修飾。這是因為NSString *有可能指向一個可變的NSMutableString 例項,如果使用的不是copy的話,那麼當可變字串被篡改後,會影響到你不可變的字串,所以要拷貝一個不可變的字串。
(4)、方法名:可以用來指定存取方法的方法名。
getter=<name>:指定獲取方法的方法名,如果某屬性是Boolean,一般會用這種方式來在獲取方法名上加上is
字首。
setter=<name>:指定設定方法的方法名,較少使用。
第7條:在物件內部儘量直接訪問例項變數
- 在物件之外訪問例項變數是通過屬性來實現,但是當在物件內部訪問例項變數的話最好採用直接訪問的形式,而在設定例項變數的時候才通過屬性來做。
//使用屬性訪問
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
- (void)setFullName {
NSArray *components = [[self fullName] componentsSeparatedByString:@" "];
self.firstName = components[0];
self.lastName = components[1];
}
//使用例項變數訪問
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}
- (void)setFullName {
NSArray *components = [[self fullName] componentsSeparatedByString:@" "];
_firstName = components[0];
_lastName = components[1];
}
-
使用例項變數和屬性訪問的區別如下:
- 使用例項變數直接訪問由於不經過方法派發,直接訪問儲存例項變數的那塊記憶體,所以速度更快。
- 直接訪問例項變數可以繞過記憶體管理語義。
- 直接訪問例項變數不會出發KVO (Key-Value Observing)。
- 通過屬性可以幫助自己debug的時候排查與之相關的錯誤。
知道以上的區別了之後,一般採用一種折中的方案,設定屬性使用“設定方法“,讀取例項變數則直接訪問。
-
在初始化的時候,如果需要設定例項變數,應當直接訪問,因為子類可能會”覆寫“設定方法,導致呼叫到子類的設定方法的時候,出現意想不到的結果。
-
如果使用了懶載入的方式,就需要使用屬性的方式來訪問例項變數。
//lazy initialization
- (NSString *)firstName {
if (!_firstName) {
_firstName = @"GDGD";
}
return _firstName;
}
第8條:理解“物件等同性”這一概念
- 由於
==
操作符比較出來的結果未必是我們想要的,因為該操作符是兩個指標本身,而不是指標所指的物件。所以一般使用NSObject協議中宣告的”isEqual“方法來判斷兩個物件的等同性。如果是自定義的類,則需要重寫該方法以實現等同性判斷。
NSString *foo = @"hhh123";
NSString *bar = [NSString stringWithFormat:@"hhh%i",123];
Boolean equalA = (foo == bar); // NO
Boolean equalB = [foo isEqual:bar];//YES
Boolean equalC = [foo isEqualToString:bar];//YES
- NSObject協議中判斷物件等同性主要是兩個方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
NSObject對這兩個方法的預設實現是:指標值完全相等的時候,兩個物件才相等。如果isEqual方法為Yes,那麼兩個物件的hash值相等,反之不成立。理解這個意義是自定義實現isEqual:
的關鍵。
設計hash方法的時候,需要儘量減少物件的碰撞,防止出現運算複雜度過大的情況。
- 特定類所具有的等同性判定方法:由於Objective-C在編譯期沒有做強型別轉換,為了增強程式碼的魯棒性,應該保證所傳物件的型別正確,且有異常處理邏輯。
- 等同性判定的執行深度:如果是對陣列進行等同性判定,往往需要比較兩個陣列的對應位置的所有物件,這樣叫做“深度等同性判定”,如果可以通過identifier來標示等同性,就能大大節省計算。
- 容器中可變類的等同性:在容器加入可變類物件的時候,把某個物件放入容器之後,就不應該改變其hash值了。由於容器會根據其hash值放在不同的“箱子陣列”裡,如果放入箱子之後hash值又改變了,那麼證明容器放錯了箱子,可能會出現隱患。所以需要保證hash值不是根據物件的可變部分來實現的,或者是在其放入容器後就不再改變hash值。
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject:arrayA];
//set = {((1,2))}
NSMutableArray *arrayB = [@[@1] mutableCopy];
[set addObject:arrayB];
//set = {((1),(1,2))}
[arrayB addObject:@2];
//set = {((1,2),(1,2))} 打破了set的語義
NSSet *copySet = [set copy];
//copySet = {((1,2))}
第9條:以”類族模式“隱藏實現細節
- 類族模式可以把實現細節隱藏在一套簡單的公共介面後面。Objective-C的系統框架中普遍使用該模式,使用者無需自己建立子類例項,只需要呼叫基類方法來建立即可。
- 一般採用工廠模式來建立類族。由於Objective-C這門語言沒辦法指明某個基類是”抽象“的,所以如果使用了類族模式需要寫上相應的註釋。
- Cocoa裡的類族:大部分collection類都是類族,如果要子類化這些類族,需要遵守一些規則:
- 子類應到繼承自類族中的抽象基類:如果要編寫NSArray類族的子類,需要繼承自不可變陣列的基類或者可變陣列的基類。
- 子類應該定義自己的資料儲存方式。
- 子類應當覆寫超類文件中指明需要覆寫的方法。
第10條:在既有類中使用關聯物件存放自定義資料
-
有時候類的例項可能是由某種機制所建立的,那麼也就是無法創建出自己所寫的子類例項,那麼也就是如果你需要存放物件到類中,就不能通過建立子類的方式實現了。而Objective-C的“關聯物件” (associated object) 可以解決這個問題。
-
可以給某個物件關聯多個物件,通過“鍵”來區分,同時在建立關聯物件的時候,也有相應的“記憶體管理語義”,由名為
objc_AssociationPolicy
的列舉所定義。關聯型別 等效的@property屬性 OBJC_ASSOCIATION_ASSIGN assign OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic、copy OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic、retain OBJC_ASSOCIATION_COPY copy OBJC_ASSOCIATION_RETAIN retain
-
-
管理關聯物件的方法:
//此方法以給定的鍵和策略為某物件設定關聯物件
- void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//此方法根據給定的鍵從某物件中獲取相應的關聯物件值
id objc_getAssociatedObject(id object, const void *key);
//此方法移除指定物件的全部關聯物件
void objc_removeAssociatedObjects(id object);
可以把物件想象成NSDictionary,把關聯到該物件的值理解為字典的條目,本質的區別在於設定關聯物件的key是個不透明的指標,在NSDictionary裡如果兩個鍵值相等那麼isEqual:
方法的返回值就是YES,但是關聯物件必須是兩個指標相同才行,在設定關聯物件的時候通常使用靜態全域性變數做鍵。
第11條:理解objc_msgSend的作用
- Objective-C的方法呼叫方式是訊息結構,這種傳遞訊息需要有“名稱”或“選擇子”,可以接受引數,而且可能還有返回值。在Objective-C中,如果向某物件傳遞訊息,那麼就會使用動態繫結機制來決定需要呼叫的方法,當物件收到訊息之後,究竟該呼叫那個方法完全由執行期決定,同時可以在程式執行時改變。
- 給物件傳送訊息的例子和過程如下:
/**
someObject —— 接受者
messageName —— 選擇子
parameter —— 引數
**/
id returnValue = [someObject messageName:parameter];
//編譯器看到這個訊息之後會轉換成一條標準的C語言函式呼叫,呼叫的是訊息中心的核心函式,objc_msgSend
void objc_msgSend(id self, SEL cmd, ...);
//所以上面那個函式呼叫經過編譯器會轉換成如下函式-->
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);
//obj_msgSend函式會依據接受者和選擇子的型別來呼叫適當的方法,為了完成此操作的方法需要在接受者所屬類中搜尋方法列表,如果有就跳到其執行的程式碼,如果沒找到就沿著體系繼續往上查詢,找到合適的方法再跳轉。如果還沒有,後面就會涉及到訊息轉發機制。
//obj_msgSend會將匹配結果換存在”快速對映表“裡,這樣每個類其實都有這樣一塊快取,所以執行起來很快,但是還是不如”靜態繫結的函式呼叫操作“那樣迅速。
- 除了上述的部分訊息的呼叫過程,還有一些”邊界情況“,則需要交由Objective-C執行環境中的一些函式來處理:
- objc_msgSend_stret:如果待發送的訊息要返回結構體,那麼可以交由此函式處理。需要CPU的暫存器能夠容納這個訊息返回的結構體,如果無法容納就會由另一個函式進行派發,那個函式會通過分配在棧上的某個變數來處理返回的結構體。
- objc_msgSend_fpret:如果訊息返回的是浮點數,那麼需要交由此函式處理。這個函式是為了處理x86等架構CPU中某些奇怪狀況。
- objc_msgSendSuper:如果要給超類發訊息,就給這個函式處理。
- 如果某函式的最後一項操作是呼叫另外一個函式,就可以使用“尾呼叫優化”技術。編譯器會生成跳轉到另一函式所需要的指令碼,而不用向呼叫堆疊裡推入新的”棧幀“。當某函式的最後一個操作僅僅是呼叫其他函式而不會將其返回值另作他用的時候,才能執行”尾呼叫優化“。
第12條:理解訊息轉發機制
-
在編譯期向類傳送了其無法解讀的訊息並不會報錯,因為執行期可以向類中新增方法,所以編譯器在編譯時無法確定類中到底會不會有某個方法實現。當物件接收到無法解讀的訊息後,就會啟動”訊息轉發“機制。
-
訊息轉發分為兩大階段:
- 第一階段先徵詢接受者所屬的類,看其是否能動態新增方法,以處理當前這個”未知的選擇子“,這部分叫”動態方法解析“。注意這是一個類方法,因為是向接收者所屬的類進行請求。
- 第二階段涉及”完整的訊息轉發機制“,這裡細分為兩小步:
- 當物件所屬類不能動態新增方法後,
runtime
就會詢問當前的接受者是否有其他物件可以處理這個未知的selector
。 - 當沒有備援接收者時,就只剩下最後一次機會,那就是訊息重定向。這個時候
runtime
會將未知訊息的所有細節都封裝為NSInvocation
物件,給接受者最後一次機會,令其設法解決當前還未處理的這條訊息。
- 當物件所屬類不能動態新增方法後,
-
動態方法解析:
- 物件收到無法解讀的訊息後,首先呼叫其所屬類的類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
這種方法的前提是:相關方法的實現程式碼已經寫好,只等著執行的時候動態插在類裡面就可以了。此方案常用來實現
@dynamic
屬性。Example:
//假設這兩個方法已經實現 id autoDictionaryGetter(id self,SEL _cmd); void autoDictionarySetter(id self, SEL _cmd, id value); //使用這個類方法進行訊息轉發 + (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selectorString = NSStringFromSelector(sel); if (/* selector is from a @dynamic property */) { if ([selectorString hasPrefix:@"set"]) { class_addMethod(self, sel, (IMP)autoDictionarySetter, "[email protected]:@"); } else { class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES; } return [super resolveInstanceMethod:sel]; }
-
備援接收者:
- 當前接受者還有第二次機會能處理未知的選擇子,在這一步中,系統會問該選擇子能不能轉發給其他接受者。相關的方法為:
- (id)forwardingTargetForSelector:(SEL)aSelector;
雖然Objective-C不支援多重繼承,但是通過這個函式的組合我們可以模擬出多次繼承的某些特性。
-
完整的訊息轉發:
- 首先建立NSInvocation物件,把尚未處理的那條訊息的所有細節都封在其中,包括選擇子、目標和引數,這個步驟會呼叫下列方法來轉發訊息:
- (void)forwardInvocation:(NSInvocation *)invocation;
實現此方法的時候,如果發現某呼叫操作不應由本類處理,則需呼叫超類的同名方法,這樣繼承體系中的每個類都有機會處理此呼叫請求,直到NSObject。
-
訊息轉發流程:
第13條:用“方法調配技術”除錯“黑盒方法”
-
與給定的選擇子名稱相對應的方法也可以在執行期改變,不需要知道原始碼,也不需要通過繼承子類來覆寫方法就能夠改變這個類本身的功能,新功能會在本類的所有例項中生效,而不僅限於覆寫了相關方法的子類例項,這個方案成為“方法調配” (method swizzling)。方法以函式指標的形式來表示,為IMP指標,原型如下:
id (*IMP)(id, SEL, ...);
-
為了互換2個已經寫好的方法實現,可以使用下列函式:
//此函式的兩個引數表示待交換的兩個方法實現
void method_exchangeImplementations(Method m1, Method m2);
//方法實現可以通過下列函式獲得。
Method class_getInstanceMethod(Class cls, SEL name);
//example
//將uppercaseString和lowercaseString兩個方法利用方法調配進行調換
Method originalMethod = class_getClassMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getClassMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
除了上面說的兩個系統方法的替換,還可以使用自定義的方法和系統方法進行替換,這樣的話就能夠為那些系統的黑盒方法增加日誌記錄功能,這個非常有助於程式除錯。很少人會在除錯程式之外的場合使用上述方法來永久改變某個類的功能。
第14條:理解“類物件”的用意
- Objective-C有個特殊的型別叫
id
,他能指代任意型別的Objective-C物件型別。編譯器假定它能響應所有訊息。id
型別本身的定義如下:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
每個物件結構體的首個成員是Class
類的變數,該變數定義了物件所屬的類,稱為"is a"指標。
Class
的定義如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
此類結構體存放類的元資料,例如類的例項實現了幾個方法,具備多少個例項變數等資訊,首個變數也是isa
指標,說明Class
本身也是Objective-C物件。結構體有個叫做super_class
的變數,是本類的超類。類物件所屬的型別是另一個類,叫做“元類”,用來表示類物件本身所具備的元資料。每個類僅有一個類物件,每個類物件僅有一個與之相關的元類。
example:
假設有個名為someClass的子類從NSObject繼承而來,則既成體系如下所示:
super_class
指標確立了繼承關係,而isa
指標描述了例項所屬的類。
-
在類繼承體系中查詢型別資訊:
- isMemberOfClass:能夠判斷出物件是否為某個特定類的例項。
- isKindOfClass:能夠判斷出物件是否為某類或者其派生類的例項。
NSMutableDictionary *dict = [NSMutableDictionary new]; [dict isMemberOfClass:[NSDictionary class]];// NO [dict isMemberOfClass:[NSMutableDictionary class]];// YES [dict isKindOfClass:[NSDictionary class]];// YES [dict isKindOfClass:[NSArray class]];// NO
如果要比較類物件是否等同的話需要使用
==
操作符,而不是isEqual:
,因為類物件是個單例,在應用程式範圍內,每個類的Class
僅有一個例項。