Objective-c方法調用流程
Objective-c是一門動態語言,動態兩個字主要就體現在我們調用方法的時候,運行時回動態尋找方法,然後調用相應的函數地址。運行時是整個Objective-c程式的基石,有了它我們的程式才能正常運行起來。
NSObject是Cocoa中絕大部分類的基類,它主要是提供了序列話,拷貝對象,以及支援運行時動態識別的架構。
在Objective-c中每一個類對象最開始的位置都會有一個isa指標,該指標指向一塊記憶體地區,該部分主要包含兩部分資訊:
1、指向父類的指標。
2、自身的方法分發表。
有了這兩部分,Objective-c的方法的調用流程就可以跑起來了。當我們調用一個對象的某一個方法的時候,首先會在當前類的分發表中尋找該方法,如果找不到對應的方法,然後再去其父類中尋找該方法,依次類推直到找到對應的方法為止,流程圖如下:
你可能會想到,如果一個類有很深的繼承層次,每次去調用根類的某個函數,豈不是都要做很多次尋找。理論上是這個樣子的,不過runtime也並非那麼傻,它會為每一個類(不是對象)維護一個經常調用的方法的列表,只要調用過就會緩衝起來(官方沒有明確說明緩衝機制),這樣當程式運行穩定以後整個方法調用的過程就會更加高效。
通過學習官方文檔Objective-C Runtime Programming Guide,可以發現其實所有的selector調用最後都會轉化為C類型的函數調用。舉個例子我們建立了一個A類型的對象aSample,然後調用其test方法([aSample test]),編譯的時候,編譯器就會將該調用轉化為objc_send(aSample, selector)的形式,runtime會調用test方法實現所對應的函數地址。該函數的參數包含了兩個隱含的參數self以及_cmd,其中self指向調用該方法的對象,_cmd則代表要調用的方法。
前面提到了NSObject提供了很多遍曆的方法可以和運行時進行互動,其中有個方法methodForSelector,通過它我們可以直接擷取到指定的方法對應的函數指標。通常我們直接使用Objective-c方式的方法調用就可以了,但有時程式中可能會頻繁的調用某一個方法,為了提高效率。我們可以直接擷取到方法對應的函數地址,然後直接調用該函數,這樣就少了動態識別的時間。
下面舉個例子:
// 父類中定義該方法- (void)testMethod{ //NSLog(@"the implementation of BaseSample!!!"); int a = 5 / 2.0f; a = ~a;}// 測試方法,分別使用兩種方法調用1億次- (void)test{ void (*methodAddress)(id,SEL); methodAddress = (void(*)(id,SEL))[self methodForSelector:@selector(testMethod)]; NSLog(@"Invoke with Method Address start!!!"); for (int i = 0; i < 100000000; ++i) { methodAddress(self, @selector(testMethod)); } NSLog(@"Invoke with Method Address finish!!!"); NSLog(@"Invoke with direct selector start!!!"); for (int i = 0; i < 100000000; ++i) { [self testMethod]; } NSLog(@"Invoke with direct selector finish!!!");}
運行結果如:
可以看出調用時間:使用函數地址調用共花費0.151s,直接調用方法花費0.734s。時間是有一點兒差距,但是已經微乎其微了,這也從側面說明了runtime的緩衝機制還是很給力的。
當我們調用某一個不存在的方法的時候,程式會crush,在命令列提示“unrecognized selector sent to instance 0xxxxxxx”,並拋出“NSInvalidArgumentException”的異常。當調用一個對象不能識別的方法時,runtime會一直沿著類的繼承關係往基類方向尋找,直到NSObject類,如果還是識別不了該方法的話,再拋出異常之前runtime還給我們了最後一次“補救”的機會。它會先調用forwardInvocation方法,如果我們想把這個方法異常調用捕獲並傳遞到其他地方的話,可以在類中重寫該方法。NSObject對於forwardInvocation方法的預設實現是調用doesNotRecognizeSelector方法,而doesNotRecognizeSelector則是直接拋出異常。
當調用forwardInvocation的時候會傳入一個NSInvocation的參數,該參數標識了調用的方法的對象以及調用的方法,並對該方法的調用結果進行封裝。我們重寫forwardInvocation方法的時候,還必須同時重寫methodSignatureForSelector方法,該方法返回表示一個方法的字串,具體如何構建請看Type Encodings。
下面舉一個簡單的重寫forwardInvocation的例子:
#pragma mark-#pragma mark 重寫 ForwardInvocation- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if ([self respondsToSelector:aSelector]) { return [super methodSignatureForSelector:aSelector]; } else { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; }}- (void)forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"Hello unreconginized selector!");}// 在init中調用一個不存在的方法hello- (id)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { // Initialization code [self hello]; } return self;}
上面的例子,截獲了不能識別的方法調用,建立了一個返回void類型的方法簽名,當調用不能識別的方法的時候列印簡單的日誌。當然在程式中最好不要這麼做,特別是開發的時候,大部分時候我們更希望能夠儘早的發現這種調用錯誤。
總結:Objective-c方法調用的流程就基本介紹完了,有什麼寫的不對的地方歡迎指正。
註:本文歡迎轉載,轉載請註明出處。同時歡迎加我qq,期待與你一起探討更多問題。