iOS探索:Runtime之訊息轉發及動態新增方法

熊貓超人發表於2018-12-13

在開始之前,我們先來了解下OC中的類與物件

268805-196560ee064edb09..jpg

這是一張經典的類的關係示意圖,接下來簡單的介紹一下這張圖

  • 首先當我們建立一個例項物件,會拷貝這個例項物件所屬類的成員變數,但是不會拷貝類定義的方法

  • 當我們傳送訊息給例項物件時,會通過這個例項物件中的isa指標去找到它對應的類,在類的方法快取中先去尋找,如果沒有命中,那麼會去方法列表中尋找,如果還是沒有命中,會通過類中的super class指標去它的父類中尋找,按照同樣的流程一直向上查詢,直至找到根類也就是NSObject,這其中需要注意的是NSObject中super class指標指向的是nil

  • 說完例項物件,那麼類物件接收到訊息時是怎樣去查詢對應方法的呢?同樣的,在類物件中也會isa指標,它指向的是類物件所對應的元類,也是先在元類的方法快取中去尋找,如果沒有命中,那麼會去方法列表中尋找,要是還沒有命中,會通過元類中的super class指標去它的父元類中尋找,按照同樣的流程一直向上查詢,直至找到根元類,這裡有一個需要注意的點是在元類中,每一個元類的isa指標都指向根元類,並且根元類的super class指標指向的是根類(NSObject),也就是說當一個類的某個類方法沒有實現,但是在根類中卻有同名的例項方法實現,這個時候就會呼叫這個同名的例項方法

個人認為只要將這張圖理解透徹就可以很好的理解類與物件的關係

吃過了開胃菜,接下來我們進入正餐

訊息轉發

出來吧,流程圖!

WX20181212-170746@2x.png

  • 對於例項方法,系統首先會回撥resolveInstanceMethod:這個方法,這個方法的引數是一個選擇器(SEL),返回值型別是BOOL型別的,告訴系統,我們要不要解決這個例項方法的實現,如果返回是YES,那麼訊息已處理,如果返回是NO,這個時候系統會給與我們第二次處理訊息的機會

  • 系統會會呼叫forwardingTargetForSelector:這個方法,這個方法的引數是一個選擇器(SEL),返回值是一個id型別,相當於告訴系統這個選擇器是具體有哪個物件來處理,如果我們指定了一個轉發目標,系統會把轉發訊息給我們的轉發目標,同時會結束當前的轉發流程,如果在第二次機會中我們依舊沒有給返回一個轉發目標,這個時候系統會給與我們第三次處理訊息的機會,也是最後一次機會

  • 系統會呼叫methodSignatureForSelector:這個方法,這個方法的引數是一個選擇器(SEL),方法的返回值是一個methodSignature,這個方法簽名實際上是對這個方法選擇器的型別,返回值和引數的一個封裝,此時,如果返回了一個方法簽名,系統會接著呼叫forwardInvocation:方法,如果能夠處理的話那麼訊息已處理,如果說methodSignatureForSelector:返回空,或者說forwardInvocation:無法處理,那麼會被標記為訊息無法處理,平時我們常見的一中crash就是無法識別選擇器,其實就是走到了最後一步還是無法處理

下面上程式碼
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    //如果呼叫的使我們的test方法
    if (sel == @selector(test)) {
        NSLog(@"resolveInstanceMethod");
        return NO;
    }else {
        return [super resolveInstanceMethod:sel];
    }
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    NSLog(@"forwardingTargetForSelector");
    //返回nil,這樣就能走到第三個步驟
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(test)) {
        NSLog(@"methodSignatureForSelector");
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }else {
        return [super methodSignatureForSelector:aSelector];
    }
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
    NSLog(@"forwardInvocation");
}
複製程式碼
  • 首先我們建立一個類繼承自NSObject,在這個類中,我們宣告一個test方法,但是並不實現,然後我們實現訊息轉發的方法

  • 首先在resolveInstanceMethod:方法中,如果這個選擇器是test,我們列印一下這個方法的名字,並且返回NO進入forwardingTargetForSelector:方法

  • 在forwardingTargetForSelector:方法中我們列印一下方法名字,然後返回nil,這樣可以進入methodSignatureForSelector:方法

  • 在methodSignatureForSelector:方法中,我們依舊判斷是否是test,如果是,我們列印下方法名字以及返回一個正確的方法簽名,關於這個方法的傳參可以看我的上一篇文章,傳送門在底下

  • 最後實現forwardInvocation:方法

我們看一下列印結果
2018-12-13 12:43:07.287974+0800 RuntimeDemo[41443:8663918] resolveInstanceMethod
2018-12-13 12:43:07.288012+0800 RuntimeDemo[41443:8663918] forwardingTargetForSelector
2018-12-13 12:43:07.288019+0800 RuntimeDemo[41443:8663918] methodSignatureForSelector
2018-12-13 12:43:07.288036+0800 RuntimeDemo[41443:8663918] forwardInvocation
複製程式碼

動態新增方法

話不多說先上程式碼
void testImp (void) {
    
    NSLog(@"test invoke");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    //如果呼叫的使我們的test方法
    if (sel == @selector(test)) {
        NSLog(@"resolveInstanceMethod");
        
        //動態新增test方法實現
        class_addMethod(self, @selector(test), testImp, "v@:");
        
        return YES;
    }else {
        return [super resolveInstanceMethod:sel];
    }
}
複製程式碼
  • 首先我們動態新增方法的時候需要在resolveInstanceMethod:方法中進行實現

  • 呼叫 class_addMethod方法,其中第一個引數是新增方法的類,這裡我們寫當前類,第二個引數是新增方法實現的方法選擇器,第三個是新增方法實現的IMP,我在上面實現了這個方法,列印了一句話,最後一個是方法字元指標,關於這個的解釋可以看我的上一篇文章,傳送門在底下

然後我們執行一下,列印結果如下
2018-12-13 13:06:33.377111+0800 RuntimeDemo[41479:8670877] resolveInstanceMethod
2018-12-13 13:06:33.377156+0800 RuntimeDemo[41479:8670877] test invoke
複製程式碼
  • 說明我們成功新增了方法實現,這裡我們已經新增方法實現,所以需要return YES,同時也就不會走到下面的方方法了

動態方法解析

@dynamic

  • 首先我們看一下@dynamic這個關鍵字

  • 當我們宣告的屬性在實現當中把它標記為@dynamic時,相當於它的set方法和get方法是在執行時新增,而不是在編譯時去給它宣告好它具體的實現

  • 動態執行時語言是將函式決議推遲到執行時,實際上就是在執行時為方法新增執行函式

  • 編譯時語言是在編譯期進行函式決議

傳送門

iOS探索:Runtime之基本資料結構

Github

Demo

相關文章