Objective-c方法調用流程


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,期待與你一起探討更多問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM