標籤:
[編寫高品質iOS代碼的52個有效方法](三)訊息和運行期
參考書籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹為快
11.理解objc_msgSend的作用
12.理解訊息轉寄機制
13.用“方法調配技術”調試“黑盒方法”
14.理解“類對象”的用意
目錄
- 編寫高品質iOS代碼的52個有效方法三訊息和運行期
- 先睹為快
- 目錄
- 第11條理解objc_msgSend的作用
- 第12條理解訊息轉寄機制
- 第13條用方法調配技術調試黑盒方法
- 第14條理解類對象的用意
第11條:理解objc_msgSend的作用
在對象上調用方法是Objective-C中經常使用的功能,用Objective-C的術語來說,叫做訊息傳遞。訊息有名稱或選取器,可以接受參數,而且可能還有傳回值。
由於Objective-C是C的超集,所以先以C語言為例來說明Objective-C中的訊息傳遞機制:
#import <stdio.h>void printHello(){ print("Hello,world!\n");}void printGoodbye(){ print("Goodbye,world!\n");}// 靜態繫結方式void staticBindingMethod(){ if(type == 0){ printHello(); } else{ printGoodbye(); } return 0;}// 動態綁定方式void dynamicBindingMethod(){ void (*fnc)(); if(type == 0){ fnc = printHello; } else{ fnc = printGoodbye; } fnc(); return 0;}
採用靜態繫結,編譯期就能決定運行時所應調用的函數,在本例中,if與else語句裡都有函數調用指令。而採用動態綁定,所調用的函數直到運行期才能確定,在本例中,只有一個函數調用指令,且調用的函數地址不確定,需要在運行期讀取出來。
在Objective-C中,如果向某對象傳遞訊息,就會使用動態綁定機制來決定需要調用的方法。Objective-C是一門真正的動態語言。給對象發送訊息可以這樣來寫:
id returnValue = [someObject messageName:parameter];
在本例中,someObject叫做接收器,messageName叫做選取器,選取器和參數合起來稱為訊息。編譯器看到此訊息後將其轉換為一條標準的C語言函數調用:
// objc_msgSend函數原型,該函數是訊息傳遞機制中的核心函數void objc_msgSend(id self, SEL cmd, ...);// 編譯器將上面例子中的訊息轉換為如下函數id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
還有一些特殊情況,交由另一些類似objc_msgSend的函數處理:
objc_msgSend_stret // 待發送的訊息返回的是結構體objc_msgSend_fpret // 待發送的訊息返回的是浮點數objc_msgSendSuper // 向超類發送訊息,也有等效於上述兩個函數的函數用於處理髮送給超類的相應訊息
如果某函數的最後一項操作是調用另一個函數,那麼就可以運用尾調用最佳化技術。編譯器會產生調轉至另一函數所需的指令碼,而且不會向呼叫堆疊中推入新的棧幀。只有當某函數的最後一個操作僅僅只是調用其他函數而不會將其傳回值另做他用時,才能執行尾調用最佳化。這項最佳化非常關鍵,可以避免過早地發生棧溢出現象。
第12條:理解訊息轉寄機制
由於Objective-C的動態特性,在編譯期向類發送了其無法解讀的訊息並不會報錯,因為在運行期可以繼續向類中添加方法,所以編譯期在編譯時間還無法確知類中到底會不會有某個方法實現。當對象接收到無法解讀的訊息後,就會啟動訊息轉寄機制。
訊息轉寄分為兩大階段,第一階段叫做動態方法解析,即徵詢接收者,所屬的類,看其是否能動態添加方法以處理當前這個未知的選取器。第二階段,運行期系統會請求接收者將訊息轉寄給其他對象來處理,又分兩小步,第一小步為快速轉寄,如果快速轉寄也不能處理,則進行第二小步標準轉寄(完整轉寄)。
// 動態方法解析調用的執行個體方法和類方法+ (BOOL)resolveInstanceMethod:(SEL)sel;+ (BOOL)resolveClassMethod:(SEL)sel;// 快速轉寄調用的方法- (id)forwardingTargetForSelector:(SEL)aSelector;// 標準轉寄調用的方法- (void)forwardInvocation:(NSInvocation *)anInvocation;
接收者在每一步中均有機會處理訊息,但步驟越往後,處理訊息的代價就越大。
下面是一個動態方法解析的樣本,以動態方式實作類別似getter和setter的方法:
//EOCAutoDictionary.h#import <Foundation/Foundation.h>@interface EOCAutoDictionary : NSObject@property (nonatomic, strong) NSString *string;@property (nonatomic, strong) NSNumber *number;@property (nonatomic, strong) NSDate *date;@property (nonatomic, strong) id opaqueObject;@end// EOCAutoDictionary.m#import "EOCAutoDictionary.h"#import <objc/runtime.h>@interface EOCAutoDictionary ()@property (nonatomic, strong) NSMutableDictionary *backingStore;@end@implementation EOCAutoDictionary// 將屬性聲明為@dynamic,則編譯器不會為其自動產生執行個體變數及存取方法@dynamic string, number, date, opaqueObject;-(id)init{ if ((self = [super init])) { _backingStore = [NSMutableDictionary new]; } return self;}// 動態方法解析+ (BOOL)resolveInstanceMethod:(SEL)sel{ NSString *selectorString = NSStringFromSelector(sel); // 根據選取器名是否以set開頭選擇動態添加的方法 if ([selectorString hasPrefix:@"set"]) { class_addMethod(self, sel, (IMP)autoDictionarySetter, "[email protected]:@"); }else{ class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES;}// 實現getterid autoDictionaryGetter(id self, SEL _cmd){ EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self; NSMutableDictionary *backingStore = typedSelf.backingStore; // getter的方法名就是屬性的名稱 NSString *key = NSStringFromSelector(_cmd); return [backingStore objectForKey:key];}// 實現settervoid autoDictionarySetter(id self, SEL _cmd, id value){ EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self; NSMutableDictionary *backingStore = typedSelf.backingStore; NSString *selectorString = NSStringFromSelector(_cmd); NSMutableString *key = [selectorString copy]; // setter的方法名轉換成屬性名稱,需要去掉末尾的冒號、開頭的set,並將屬性名稱的開頭的大寫字母轉為小寫 // 如 date 屬性的setter方法名為 setDate: [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)]; [key deleteCharactersInRange:NSMakeRange(0, 3)]; NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar]; if (value) { [backingStore setObject:value forKey:key]; }else{ [backingStore removeObjectForKey:key]; }}@end
快速轉寄和標準轉寄的實現可以分別參考:
快速轉寄樣本
標準轉寄樣本
第13條:用“方法調配技術”調試“黑盒方法
類的方法列表會把選取器的名稱映射到相關的方法實現之上,使得動態訊息派發系統能夠據此找到應調用的方法。Objective-C運行期系統提供了幾個方法可以操作選取器的映射表,可以新增選取器,改變選取器對應的方法實現,或者交換兩個選取器所映射的指標。這就是方法調配。
下面是用方法調配技術來交換NSString中的大小寫轉換方法:
#import <Foundation/Foundation.h>#import <objc/runtime.h>int main(int argc, const char * argv[]) { @autoreleasepool { // 從類中擷取兩個方法,再將其進行交換 Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString)); method_exchangeImplementations(originalMethod, swappedMethod); NSString *string = @"ThIs iS tHe StRiNg"; NSString *lowercaseString = [string lowercaseString]; NSLog(@"lowercaseString = %@", lowercaseString); NSString *uppercaseString = [string uppercaseString]; NSLog(@"uppercaseString = %@", uppercaseString); } return 0;}
運行結果:
2016-07-25 14:21:25.058 OCTest[31026:1747037] lowercaseString = THIS IS THE STRING2016-07-25 14:21:25.059 OCTest[31026:1747037] uppercaseString = this is the string
可以用這個方法來為完全不透明的黑盒方法增加日誌記錄功能,這非常有助於程式調試。下面就為lowercaseString方法添加日誌記錄功能:
#import <Foundation/Foundation.h>#import <objc/runtime.h>// 在NSString分類中添加並實現一個新方法@interface NSString(EOCMyAdditions)- (NSString*)eoc_myLowercaseString;@end@implementation NSString (EOCMyAdditions)- (NSString*)eoc_myLowercaseString{ // 看似會陷入死迴圈,但運行期進行調換後,實際調用的是lowercaseString方法 NSString *lowercase = [self eoc_myLowercaseString]; NSLog(@"%@ => %@", self, lowercase); return lowercase;}@endint main(int argc, const char * argv[]) { @autoreleasepool { // 調換方法 Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString)); method_exchangeImplementations(originalMethod, swappedMethod); NSString *string = @"ThIs iS tHe StRiNg"; // 現在調用lowercaseString方法會輸出日誌 [string lowercaseString]; } return 0;}
運行結果:
2016-07-25 14:30:29.847 OCTest[31568:1753042] ThIs iS tHe StRiNg => this is the string
注意:只有偵錯工具的時候才需要在運行期修改方法實現,這種做法不宜濫用。
第14條:理解“類對象”的用意
物件類型並非在編譯期就綁定好了,而是要在運行期尋找。在運行期檢視物件類型這一操作也叫做類型資訊查詢,這個強大而有用的特性內建於NSObject協議中,凡是有公用根類(NSObject與NSProxy)繼承而來的對象都要遵從此協議。
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
下面是用類型資訊查詢根據數組中儲存的對象產生以逗號分隔的字串:
-(NSString*)commaSeparatedStringFromObjects:(NSArray*)array{ NSMutableString *string = [NSMutableString new]; for (id object in array) { if ([object isKindOfClass:[NSString class]]) { [string appendFormat:@"%@,", object]; }else if ([object isKindOfClass:[NSNumber class]]){ [string appendFormat:@"%d,", [object intValue]]; }else if ([object isKindOfClass:[NSData class]]){ NSString *base64Encoded = /* base64 encodes data */ [string appendFormat:@"%@,",base64Encoded]; }else{ // 處理不支援類型 } } return string;}
比較類對象是否等同是,不要使用“isEqual:”方法,因為類對象是單例,在應用程式範圍內,每個類的Class僅有一個執行個體,另一種精確判斷物件類型的辦法是使用“==”:
id object = /* ... */if ([object class] == [EOCSomeClass class]){ // code}
但更好的方法仍舊是使用類型資訊查詢方法,因為它可以正確處理那些使用了訊息傳遞機制的對象。如果在代理對象上調用class方法,那麼返回的是代理對象本身(NSProxy的子類),而非接受代理的對象所屬的類。若是改用isKindOfClass:等類型資訊查詢方法,那麼代理對象就會把這條訊息轉寄給接受代理的對象,也就是說,這條訊息的傳回值與直接在接受代理的對象上查詢其類型所得的結果相同。
[編寫高品質iOS代碼的52個有效方法](三)訊息和運行期