iOS探索:Runtime之消息轉發及動態添加方法

NO IMAGE

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

iOS探索:Runtime之消息轉發及動態添加方法

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

  • 首先當我們創建一個實例對象,會拷貝這個實例對象所屬類的成員變量,但是不會拷貝類定義的方法

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

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

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

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

消息轉發

出來吧,流程圖!

iOS探索:Runtime之消息轉發及動態添加方法

  • 對於實例方法,系統首先會回調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:"[email protected]:"];
}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, "[email protected]:");
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

相關文章

iOSCoreData(一)增刪改查

iOS探索:網絡相關

iOS探索:RunLoop本質、數據結構以及常駐線程實現

iOS探索:Block解析淺談