調用方法(函數)是語言常用的功能,在 Objective-C 中專業一點的叫法是 傳遞消息(pass a message)。Objective-C 的方法調用都是 動態綁定 ,而C語言中函數調用方式是 靜態綁定 ( static binding ),也就是說,在編譯時期就能決定和知道在運行時所調用的函數。ios
如下面代碼爲例:c++
void sayHello(){ } void sayGoodBye(){ } void saySomething(int type){ if(type == 0){ sayHello(); }else{ sayGoodBye(); } }
基本上,上面的代碼在編譯的時候編譯器就知道 sayHello 和 sayGoodBye 兩個函數的存在,函數地址是硬編碼在指令之中的。可是若是換一種寫法:編程
void sayHello(){ } void sayGoodBye(){ } void saySomething(int type){ void (*something) (); if(type == 0){ something = sayHello; }else{ something = sayGoodBye; } something(); }
這就得使用 動態綁定 ,待調用的函數地址須要到運行時才能讀取出來。
在 Objective-C 中,對某一個對象傳遞消息,會用動態綁定機制來決定究竟是調用哪一個方法。而Objective-C是 C 的超集,底層是由 C語言實現,可是對象接收消息後會調用哪一個方法都是在運行期決定。緩存
給對象發送消息能夠這麼來寫:app
id object = [list objectAtIndex:1];
在這行代碼中, list 稱爲 接收者, objectAtIndex 叫作 選擇器, 選擇器和參數合起來稱爲消息。當編譯器看到這行代碼的時候,會換成標準的C語言函數調用:函數
void objc_msgSend(id self, SEL cmd, ...); id lastObject = objc_msgSend(list, @selector(objectAtIndex:), parameter);
objc_msgSend 這個函數能夠接收兩個及兩個以上的參數,第一個參數是接收者,第二個參數是選擇器,後面的參數是保持順序的原來消息傳遞的參數,objc_msgSend會依據接收者和選擇器來決定調用哪一個方法,首先在接收者的方法列表中尋找,若是找不到就會沿着繼承體系去向上一層一層的尋找,若是仍舊找不到就會執行消息轉發(message forwarding) 。
當消息第一次傳遞以後,objc_msgSend 會將匹配結果進行緩存,下次會直接調用方法。消息傳遞除了objc_msgSend以外在特殊狀況下還會有其餘的方法來處理:測試
objc_msgSend_stret 若是待發送的消息返回一個結構體,就會調用這個函數來處理。編碼
objc_msgSend_fpret 若是消息返回的是浮點數,就會調用這個函數進行處理。atom
objc_msgSendSuper 若是要傳遞消息給父類。spa
總結:
消息由 接收者、選擇器及參數構成,給某對象 發送消息( invoke a message ) 也就至關於在該對象上調用方法。
發送給某對象的所有消息都要有動態消息派發系統( dynamic message dispatch system ) 來處理。
在上面介紹了運行時的消息傳遞機制,可是卻沒有說對象收到消息卻沒法解讀該怎麼辦。本篇博客就着重介紹當消息傳遞時沒法解讀的時候就會啓動的 消息轉發機制( message forwarding )。
開發可能常常會遇到這種狀況:
2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50' *** First throw call stack: ( 0 CoreFoundation 0x00007fff9f2d94f2 __exceptionPreprocess + 178 1 libobjc.A.dylib 0x00007fff90db3f7e objc_exception_throw + 48 2 CoreFoundation 0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205 3 CoreFoundation 0x00007fff9f249571 ___forwarding___ + 1009 4 CoreFoundation 0x00007fff9f2490f8 _CF_forwarding_prep_0 + 120 5 runtime 0x0000000100001c1c main + 124 6 libdyld.dylib 0x00007fff91df85ad start + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
這個異常信息是由 NSObject 的 doesNotRecognizeSelector: 方法拋出來的,原本是給 AutoDictionary 的一個實例對象發送消息,可是該對象並無 setDate: 方法,因此消息轉發給了 NSObject ,最後拋出異常。
先看下消息處理機制流程圖:
消息轉發分爲兩階段三步,第一階段先看接受消息的對象能不能本身處理這個沒法解讀的消息,這一步能夠動態的添加方法去解讀接受這個消息;第二階段是先看看對象本身不能處理這個消息,能不能交給其餘對象來進行處理,在這一步若是仍然沒法解讀消息,那麼就會走最後一步:把和消息有關的全部細節封裝到一個 NSInvocation 中,再詢問一次對象是否能解決。
看下三個方法:
// 詢問對象是否本身處理,是返回YES,通常會在這個方法裏面動態添加方法 + (BOOL)resolveInstanceMethod:(SEL)sel; // 這一步詢問對象把消息交給哪一個對象來進行處理 - (id)forwardingTargetForSelector:(SEL)aSelector; // 若是走到這一步的話,就把消息的全部信息封裝成 NSInvocation 對象進行 "最後通牒" - (void)forwardInvocation:(NSInvocation *)anInvocation;
來一段代碼示例:
新建一個 AutoDictionary 類,添加一個 NSDate 類型的 date 屬性,在實現文件裏面用 @dynamic date; 禁止自動生成存取方法,這樣當代碼中給 AutoDictionary 實例對象的 date屬性賦值時就會出現消息沒法解讀的現象。
.h 文件:
@interface AutoDictionary : NSObject @property (nonatomic, strong) NSDate *date; @end
.m 實現文件代碼內容:
@interface AutoDictionary() @property (nonatomic, strong) NSMutableDictionary *backingStore; /** * 該類僅在實現文件 實現了 * - (NSDate *)date * - (void)setDate:(NSDate *)date * 兩個方法,用於處理 AutoDictionary 沒法解讀的消息 */ @property (nonatomic, strong) MethodCreator *methodCreator; @end @implementation AutoDictionary @dynamic date; - (instancetype)init{ if (self = [super init]) { self.backingStore = [NSMutableDictionary dictionary]; self.methodCreator = [MethodCreator new]; } return self; } #pragma mark - 消息轉發機制 :1.動態添加方法 2.後備消息接收者 3.封裝NSInvocation,最後通牒 // 3. 封裝NSInvocation,最後通牒 - (void)forwardInvocation:(NSInvocation *)anInvocation{ } // 2. 沒法接受消息,選擇由誰來接受 - (id)forwardingTargetForSelector:(SEL)aSelector{ return self.methodCreator; } // 1. 動態添加方法 + (BOOL)resolveInstanceMethod:(SEL)sel{ NSString *selString = NSStringFromSelector(sel); if ([selString hasPrefix:@"set"]) { class_addMethod(self, sel, (IMP)autoDictSetter, ""); }else{ class_addMethod(self, sel, (IMP)autoDictGetter, ""); } return YES; } id autoDictGetter (id self, SEL _cmd){ AutoDictionary *dict = self; NSString *key = NSStringFromSelector(_cmd); return [dict.backingStore objectForKey:key]; } void autoDictSetter (id self, SEL _cmd, id value){ AutoDictionary *dict = self; NSString *selString = NSStringFromSelector(_cmd); NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)]; key = [key lowercaseStringWithLocale:[NSLocale currentLocale]]; if (value) { [dict.backingStore setObject:value forKey:key]; }else{ [dict.backingStore removeObjectForKey:key]; } } @end
測試代碼:
AutoDictionary *dict = [AutoDictionary new]; dict.date = [NSDate date]; NSLog(@"dict.date = %@",dict.date);
在開發中有時候想給對象實例添加個變量來存儲數據,但又沒法直接聲明,好比說既有類的分類。這個時候咱們就能夠經過 關聯對象 在運行時給對象關聯一個 對象 來存儲數據。(注意:並非真實的添加了一個實例變量)
關聯對象 能夠給某個對象關聯其餘對象並用key來區分其餘對象。須要注意的是,存儲對象的時候要指明 存儲策略,用來維護對象的內存管理語義。存儲策略是 objc_AssociationPolicy 枚舉定義,如下是存儲策略對應的 @property屬性:
存儲策略類型 | 對應的@property屬性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | weak |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
OBJC_ASSOCIATION_RETAIN | strong |
OBJC_ASSOCIATION_COPY | copy |
用下面的方法能夠管理關聯對象:
// 這個方法能夠根據指定策略給對象關聯對象值 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)
對於關聯對象這個OC特性,咱們能夠把對象想象成一個 NSDictionary,關聯對象須要一個 key( 類型是 opaque pointer,無類型的指針 ) 來區分,咱們能夠把要添加的變量名做爲 key ,把變量的值做爲關聯的對象來存儲到 」對象「 這個 NSDictionary 中。
因此,關聯對象的
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
方法相似於字典的
[dict setObject: forKey:]
方法。
在存儲和獲取關聯對象時須要用一個相等的 key ,由於是給 Class 的實例對象關聯對象,因此通常用靜態變量來作 key 。
說的再多,不如上段代碼!
好比說,咱們給 NSString 實例加上個 NSDate 類型的 date 變量。什麼?給字符串加個日期變量是要幹嫋?我要給字符串過個生日不行嗎! 別鬧,舉個栗子嘛!(捂臉逃跑~~~)
首先,咱們先給 NSString 新建個名爲 RT 的 category。
在頭文件中有個 NSDate 類型的 date 屬性:
// NSString+RT.h // runtime #import <Foundation/Foundation.h> @interface NSString (RT) @property (nonatomic, strong) NSDate *date; @end
在分類中的屬性只會生成 get 和 set 方法,並不會生成變量。
因此咱們須要重寫 get 和 set 方法,關聯對象以變相實現添加變量,在現實文件中:
// NSString+RT.m // runtime #import <objc/runtime.h> #import "NSString+RT.h" @implementation NSString (RT) static void *runtime_date_key = "date"; - (NSDate *)date{ return objc_getAssociatedObject(self, runtime_date_key); } - (void)setDate:(NSDate *)date{ objc_setAssociatedObject(self, runtime_date_key, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
須要注意的是,關聯對象用到的 key 是個無類型的指針,通常來講是靜態來修飾。
另外,給對象關聯的只能是對象,若是是 int、 float 等類型須要 NSNumber 進行包裝。
由於 date 是強引用和非原子屬性,因此關聯策略用 OBJC_ASSOCIATION_RETAIN_NONATOMIC
而後執行代碼:
NSString *string = @"runtimeTestString"; string.date = [NSDate date]; NSLog(@"string.date = %@",string.date);
輸出結果:
2016-04-12 21:27:31.099 runtime[2837:103727] string.date = 2016-04-12 13:27:31 +0000
注意:
定義關聯對象時須要指定內存管理語義,用來模擬對象對變量的擁有關係
儘可能避免使用關聯對象,由於若是出現bug不易於問題排查
在 Objective-C 中,類的方法列表會把選擇器的名稱映射到方法的實現上,這樣 動態消息轉發系統 就能夠以此找到須要調用的方法。這些方法是以函數指針的形式來表示,這種指針叫作 IMP。
以下:
id (*IMP) (id, SEL, ...)
Objective-C 的 runtime 機制以此提供了獲取和交換映射IMP的的接口:
// 獲取方法 Method class_getInstanceMethod(Class cls, SEL name); // 交換兩個方法 void method_exchangeImplementations(Method m1, Method m2)
咱們能夠經過上面兩個方法來進行選擇器和所映射的IMP進行交換:
來,直接上代碼示例,好比咱們的要實現功能是在每一個控制器的viewDidLoad方法裏面log一下,通常有三種實現方式:
直接修改每一個頁面的 view controller 代碼,簡單粗暴;
子類化 view controller ,並讓咱們的 view controller 都繼承這些子類;
使用 Method Swizzling 進行 hook,以達到 AOP 編程的思想
第一種實現的代碼是在每一個類的裏面都這麼寫:
- (void)viewDidLoad { [super viewDidLoad]; DDLog(); }
第二種是隻在基類裏面寫。而後全部的控制器都繼承這個基類。
最後一種是最佳的解決方案:
@implementation UIViewController (Log) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(log_viewDidLoad); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } #pragma mark - Method Swizzling - (void)log_viewDidLoad{ [self log_viewDidLoad]; DDLog(...); } @end
注意:
爲何使用 + (void)load ?由於父類、子類和分類的該方法是分別調用,互不影響,並且是在類被加載的時候一定會調用的方法。