探祕Runtime - Runtime Message Forward

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/f313e8e32946html


博客配圖


當一個對象的方法被調用時,首先在對象所屬的類中查找方法列表,若是當前類中沒有則向父類查找,一直找到根類NSObject。若是始終沒有找到方法實現,則進入消息轉發步驟中。git

動態消息解析

當一個方法沒有實現時,也就是在cache lsit和其繼承關係的method list中,沒有找到對應的方法。這時會進入消息轉發階段,可是在進入消息轉發階段前,Runtime會給一次機會動態添加方法實現。github

能夠經過重寫resolveInstanceMethod:resolveClassMethod:方法,動態添加未實現的方法。其中第一個是添加實例方法,第二個是添加類方法。這兩個方法都有一個BOOL返回值,返回NO則進入消息轉發機制。app

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:sel];
}
複製代碼

在經過class_addMethod函數動態添加實現時,後面有一個"v@:"來描述SEL對應的函數實現,具體的描述能夠參考官方文檔ide

Message Forwarding

Message Forward

在進行消息轉發以前,還能夠在forwardingTargetForSelector:方法中將未實現的消息,轉發給其餘對象。能夠在下面方法中,返回響應未實現方法的其餘對象。函數

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorName = NSStringFromSelector(aSelector);
    if ([selectorName isEqualToString:@"selector"]) {
        return object;
    }
    return [super forwardingTargetForSelector:aSelector];
}
複製代碼

forwardingTargetForSelector:方法未作出任何響應的話,會來到消息轉發流程。消息轉發時會首先調用methodSignatureForSelector:方法,在方法內部生成NSMethodSignature類型的方法簽名對象。在生成簽名對象時,能夠指定targetSEL,能夠將這兩個參數換成其餘參數,將消息轉發給其餘對象。佈局

[otherObject methodSignatureForSelector:otherSelector];
複製代碼

生成NSMethodSignature簽名對象後,就會調用forwardInvocation:方法,這是消息轉發中最後一步了,若是在這步尚未對消息進行處理,則會致使崩潰。學習

這個方法中會傳入一個NSInvocation對象,這個對象就是經過剛纔生成的簽名對象建立的,能夠經過invocation調用其餘對象的方法,調用其invokeWithTarget:便可。ui

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([object respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:object];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
複製代碼

消息轉發

將一條消息發送給一個不能處理的對象會引發崩潰,可是在崩潰以前,系統給響應對象一次處理異常的機會。編碼

當發送一條對象不能處理的消息,產生Crash以前,系統會調用響應者的forwardInvocation:方法,並傳入一個NSInvocation對象,NSInvocation對象中包含原始消息及參數。這個方法只有方法未實現的時候纔會調用。

你能夠實現forwardInvocation:方法,將消息轉發給另外一個對象。forwardInvocation:方法是一個動態方法,在響應者沒法響應方法時,會調用forwardInvocation:方法,能夠重寫這個方法實現消息轉發。

消息轉發中forwardInvocation:須要作的是,確認消息將發送到哪裏,以及用原始參數發送消息。能夠經過invokeWithTarget:方法,發送被轉發的消息。調用invoke方法後,原方法的返回值將被返回給調用方。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:someOtherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
複製代碼

forwardInvocation:方法不僅能夠處理一個方法,能夠經過Selector進行判斷,來處理多個須要轉發的方法。

動態方法解析

在OC中有時候能夠動態的提供方法實現,例如屬性能夠經過@dynamic propertyName;的形式,表示將在運行過程當中動態的提供屬性方法。

若是想實現動態方法解析,須要實現resolveInstanceMethod:resolveClassMethod:方法,在這兩個方法中動態的添加方法實現。經過class_addMethod方法能夠動態添加方法,添加方法時須要關聯對應的函數指針,函數指針須要聲明兩個隱藏參數self_cmd

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL {
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSel];
}
複製代碼

消息轉發和動態方法解析大部分是相同的,在消息轉發以前一個類有機會動態解析這個方法。若是已經調用respondsToSelector:instancesRespondToSelector:方法,動態方法解析有機會優先爲Selector添加IMP。若是你實現了resolveInstanceMethod:方法,但想要特定的Selector走消息轉發流程,則將此方法返回NO便可。

轉發和多繼承

能夠經過消息轉發機制來模擬多繼承,例以下面這張圖中,兩個類中雖然不存在繼承關係,可是卻由另外一個類處理了Warrior的事件。

多繼承

由上面的例子能夠看出,分屬兩個繼承分支的類,經過消息轉發機制實現了繼承的關係。Warriornegotiate消息由其「父類」Diplomat來實現。

經過消息轉發實現的多重繼承相對於普通繼承來講更有優點,消息轉發能夠將消息轉發給多個對象,這樣就能夠將代碼按不一樣職責封裝爲不一樣對象,並經過消息轉發給不一樣對象處理。

須要注意的是,Warrior雖然經過消息轉發機制能夠響應negotiate消息,但若是經過respondsToSelector:isKindOfClass:方法進行判斷的話,依然是返回NO的。若是想讓這兩個方法能夠在判斷negotiate方法時返回YES,須要重寫這兩個方法並在其中加入判斷邏輯。

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([super respondsToSelector:aSelector]) {
        return YES;
    } else {
        // 
    }
    return NO;
}
複製代碼

在執行forwardInvocation:以前,須要經過methodSignatureForSelector:方法返回方法簽名,若是不實現則用默認的方法簽名。

在方法簽名的過程當中,能夠將未實現的方法轉發給其代理。

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}
複製代碼

runtime類型編碼

使用技巧

在項目中常常會出現由於調用未實現的方法,致使程序崩潰的狀況。在學習消息轉發後,就能夠經過消息轉發來解決這個問題。

全部的類的基類都是NSObject類(NSProxy除外),能夠將NSObject類的消息轉發流程攔截,而後作一些統一的處理,這樣就能夠解決方法未實現致使的崩潰。根據Category能夠將原類方法「覆蓋」的特色,能夠在Category中實現相應的攔截方法。

// 自定義類
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
- (void)testMethod;
@end

// 接收消息的IMP
void dynamicResolveMethod(id self, SEL _cmd) {
    NSLog(@"method forward");
}

// 對NSObject建立的Category
@implementation NSObject (ExceptionForward)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    const char *types = sel_getName(sel);
    class_addMethod([self class], sel, (IMP)dynamicResolveMethod, types);
    return YES;
}

#pragma clang diagnostic pop

@end
複製代碼

咱們的攔截方案是在resolveInstanceMethod方法中,動態建立未實現的方法,並將IMP統一設置爲dynamicResolveMethod函數進行處理。這樣全部未實現的方法都會執行dynamicResolveMethod函數,而不崩潰,在dynamicResolveMethod函數中能夠作崩潰統計等操做。

多繼承

在OC中是不支持多繼承的,可是能夠經過消息轉發模擬多繼承。在子類中實例化多個父類,當消息發送過來的時候,在消息轉發的方法中,將調用重定向到父類的實例對象中,以實現多繼承的效果。

下面是多繼承的例子,建立兩個父類CatDog,並將須要子類繼承的方法都定義到Protocol中,在CatDog中實現Protocol中的方法。

@protocol CatProtocol <NSObject>
- (void)eatFish;
@end

@interface Cat : NSObject <CatProtocol>
@end

@implementation Cat
- (void)eatFish {
    NSLog(@"Cat Eat Fish");
}
@end

@protocol DogProtocol <NSObject>
- (void)eatBone;
@end

@interface Dog : NSObject
@end

@implementation Dog
- (void)eatBone {
    NSLog(@"Dog Eat Bone");
}
@end
複製代碼

子類直接經過遵照父類的協議,來表示本身「繼承」哪些類,並在內部實例化對應的父類對象。在外界調用協議方法時,子類實際上是沒有實現這些父類的方法的,因此經過轉發方法將消息轉發給響應的父類。

@interface TestObject : NSObject <CatProtocol, DogProtocol>
@end

@interface TestObject()
@property (nonatomic, strong) Cat *cat;
@property (nonatomic, strong) Dog *dog;
@end

@implementation TestObject

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.cat respondsToSelector:aSelector]) {
        return self.cat;
    } else if ([self.dog respondsToSelector:aSelector]) {
        return self.dog;
    } else {
        return self;
    }
}

// 忽略Cat和Dog的初始化過程
@end
複製代碼

簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github上,下載Runtime PDF合集。把全部Runtime文章總計九篇,都寫在這個PDF中,並且左側有目錄,方便閱讀。

Runtime PDF

下載地址:Runtime PDF 麻煩各位大佬點個贊,謝謝!😁

相關文章
相關標籤/搜索