聊聊NSInvocation和NSMethodSignature

前言

咱們這裏不會通過源碼介紹Runtime,已經有很多文章介紹了,而且太晦澀,讀起來不舒服,也不會介紹Runtime的一些基本原理,這個作爲iOS開發最熟悉了,只是通過一些我們平時用到的操作,來宏觀的介紹NSInvocationNSMethodSignature,隨便聊聊,做一些簡單的記錄,還記得剛接觸這個的時候咱們腦海裏面的問號嗎?

什麼是方法,什麼是選擇器,什麼是方法簽名,什麼是IMP,什麼是消息?下面簡單的回顧下

Selector

選擇器是方法的名稱。你肯定對以下選擇器非常熟悉:allocinitreleasedictionaryWithObjectsAndKeys:setObject:forKey:等,而且冒號是選擇器的一部分。這就是我們確定此方法需要參數的方式。不過你也可以不帶參數名,但是這樣做不推薦doFoo :::。這是一個帶有三個參數的方法,可以像[someObject doFoo:arg1:arg2:arg3]一樣調用它。不需要在選擇器的每個部分之前都包含字母。Cocoa框架下
它們具有SEL類型:SEL aSelector = @selector(doSomething :)SEL aSelector = NSSelectorFromString(@“ doSomething:”)

Method Signature

方法簽名叫起來比較專業,其實他就是一個記錄方法返回值和參數的數據類型罷了。 他們可以在運行時用NSMethodSignature和C的char *來表示

Message

消息就是上面的提到的選擇器加上你要隨着選擇器發送的參數。比如[dict setObject:obj forKey:key],這個消息就是選擇器 SEL aSelector = @selector(setObject: forKey:),加上參數obj和key。可以將消息封裝在NSInvocation中,這兩點就是提到的SELarguments,選擇器 + 參數列表,後續還有Targetreturn value進一步介紹,這裏還涉及到方簽名。

Method

struct objc_method {
    SEL _Nonnull method_name       //方法名                        
    char * _Nullable method_types  //方法簽名                      
    IMP _Nonnull method_imp        // 方法實現               
} 

方法是選擇器(SEL)和實現(IMP)的組合。IMP其實就是一個函數指針。我個人理解,這裏的method_types就是我們下面要提到的SEL對應的方法簽名

Implementation

方法實際的可執行代碼。他在運行時用IMP表示,實際上就是一個函數指針。

NSMethodSignature

A record of the type information for the return value and parameters of a method.
一個對於方法返回值和參數的記錄。 也可以叫做一個對於方法的簽名

第一種形態

介紹NSInvocation之前,咱們先來了解下這個類NSMethodSignature,根據上面文檔的介紹,主要是記錄了方法的返回值和參數。咱們先來看看生成一個NSMethodSignature所需要的步驟

根據頭文件提供的兩個方法,一個+方法一個-方法

NSMethodSignature *sign1 = [@"" methodSignatureForSelector:@selector(initWithFormat:)];
NSMethodSignature *sign2 = [NSClassFromString(@"NSString") instanceMethodSignatureForSelector:@selector(initWithFormat:)];
NSLog(@"%@---%@",sign1,sign2);

打印結果如下:

(lldb) po sign1
<NSMethodSignature: 0x600001fb42a0>
    number of arguments = 3
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

2020-05-18 17:15:32.545730+0800 YTKNetworkDemo[14057:11030644] <NSMethodSignature: 0x600001fb42a0>---<NSMethodSignature: 0x600001fb42a0>

這裏可以看出,同一個方法,無論哪種方式拿到的方法簽名對象都是一樣的,而且這裏還有個小知識點po和直接NSLog打印的時候,爲什麼不同呢?這是因爲NSObject提供了兩個協議方法

@property (readonly, copy) NSString *description;
@optional
@property (readonly, copy) NSString *debugDescription;

description專門是用來爲log服務的,而debugDescription就體現在lldb上面的調試指令。

除了這個不同,我們還從打印的日誌中看到了type encoding (@) '@',先看下如下代碼

Method m = class_getInstanceMethod(NSString.class, @selector(initWithFormat:));
const char *c = method_getTypeEncoding(m);

打印@24@0:8@16,你肯定會有下面的疑惑

疑惑點

1.這裏的@符號代表什麼?
2.這裏的數字代表什麼
3.SEL + Arguments 的消息原型 <return_type>Class_selector(id self ,SEL _cmd,...)objc_msgSend的原型(prototypevoid objc_msgSend(id self,SEL cmd,...)有什麼關聯,爲什麼長的差不多?

TypeEncoding 方法編碼( 輔助簽名)

爲了輔助運行時系統,編譯器對字符串中每個方法的返回和參數類型進行編碼,並將字符串與方法選擇器相關聯

OC類型編碼表

例如NSString的類方法isEqualToString: 的方法簽名爲B24@0:8@16

  1. @encode(BOOL) (B) 返回值

  2. @encode(id) (@) 默認第一個參數 self

  3. @encode(SEL) (:)默認第二個參數 _cmd

  4. @encode(NSString *) (@) 實際上的第一個參數NSString

那麼下面的打印就很容易理解了

'NSString'|'initWithFormat:locale:arguments:' of encoding '@40@0:8@16@24[1{__va_list_tag=II^v^v}]32'
'NSString'|'initWithCoder:' of encoding '@24@0:8@16'
'NSString'|'initWithString:' of encoding '@24@0:8@16'

方法簽名包含一個或多個用於方法返回類型的字符,後跟隱式參數self和_cmd的字符串編碼,後跟零個或多個顯式參數。

[返回值][target][action][參數]

解惑點

1.各種符號就是參數類型的字符串編碼,方便與SEL關聯,而且OC方法默認帶了self_cmd這兩個參數,所以這也是爲什麼能直接在方法中用這兩個”關鍵字”的原因,所以配合上面的編碼表 B(返回值)24@(self)0:(_cmd)8@(第一個參數NSString)

2.根據上面的offset描述,可以揣測出大概的意思是類基地址的偏移,比如上面的@24代表返回值,一般在最後,其中@0代表基地址偏移0,指針變量8個字節,然後:8代表_cmd,再然後@16代表對應的參數,這裏的參數是字符串,因此就只有8個字節,如果你用NSRange可以試試,這裏就會擴充出16個字節,裏面存了兩個unsign long long類型

3.根據上面的疑惑點,發送消息是轉換成void objc_msgSend(id self,SEL cmd,...),會根據接受者和SEL選擇器來調用適當的方法。那麼一旦找到對應的方法實現之後,會直接跳轉過去,之所以能這樣是因爲Objective-C對象的每個方法都可以視爲簡單的C函數,其原型如下 <return_type>Class_selector(id self ,SEL _cmd,...)。每個類中都有一張表格,其中的指針都會指向這種函數,而選擇器對應的名稱就是查表的Key,SEL可以簡單理解爲字符串 + 簽名。其中這裏原型的樣子和objc_msgSend很像,這顯然不是巧合,這是爲了利用尾調用優化(tail-call optimization),另方法跳轉變得更加簡單。如果函數的最後是調用另一個函數,那麼久可以利用尾調用優化技術。編譯器會生成調轉另一個函數所需的指令碼,而且不會向調用堆棧中推入新的棧幀。只有當函數的最後一個操作僅僅是調用其他函數而不會將其返回值另做他用,纔可以執行尾調用優化。別小看這個優化,如果不這麼做,那麼每次調用OC的方法,都要爲objc_msgSend函數準備棧幀,若不優化,很容易發生Stack OverFlow

案例分析

根據上面的介紹,NSMethodSignature本質上就是對方法返回值和參數的簽名。那麼下面根據Runtime的消息轉發,來動態給類添加一個方法。

@interface MKJAutoDictionary : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) id obj;

@end

@interface MKJAutoDictionary ()

@property (nonatomic, strong) NSMutableDictionary *storeDict;

@end

@implementation MKJAutoDictionary
@dynamic name, obj;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _storeDict = [[NSMutableDictionary alloc] init];
    }
    return self;
}

就這樣聲明瞭一個類,目的是調用屬性的方法時,自動存儲到字典裏面。這裏我們給新增的兩個屬性nameobj設置爲@dynamic,這個關鍵字就不介紹了,我們的另一個置頂博客有介紹。那麼當外面調用時

extern void instrumentObjcMessageSends(BOOL);
instrumentObjcMessageSends(YES);        
MKJAutoDictionary *atd = [[MKJAutoDictionary alloc] init];
[atd performSelector:@selector(setName:) withObject:@"123456"];
instrumentObjcMessageSends(NO);

這裏肯定會蹦,由於我們給了dynamic關鍵字,那麼這裏有一個多餘的函數,主要我是用來證明是否進入消息轉發。
這裏有個傳送門介紹方法
根據介紹我們可以在private/tmp目錄下找到msgSends-xxxxx的一個日誌文件,打開就能在裏面看到整個消息調用過程。

...
- MKJAutoDictionary NSObject performSelector:withObject:
+ MKJAutoDictionary MKJAutoDictionary resolveInstanceMethod:
+ MKJAutoDictionary MKJAutoDictionary resolveInstanceMethod:
- MKJAutoDictionary NSObject forwardingTargetForSelector:
- MKJAutoDictionary NSObject forwardingTargetForSelector:
- MKJAutoDictionary NSObject methodSignatureForSelector:
- MKJAutoDictionary NSObject methodSignatureForSelector:
- MKJAutoDictionary NSObject class
- MKJAutoDictionary NSObject doesNotRecognizeSelector:
- MKJAutoDictionary NSObject doesNotRecognizeSelector:
- MKJAutoDictionary NSObject class
...

上面只是一個簡單的小插曲,告訴大家有個方法能打印整個調用過程。
下面我們通過Runtime的消息轉發,動態給類添加一個方法,來理解下Method結構體以及上面提到的簽名是如何使用的

void autoDictSetter(id self, SEL _cmd, id value){
    MKJAutoDictionary *dict = (MKJAutoDictionary *)self;
    [dict.storeDict setValue:value forKey:@"123"];
}
id autoDictGetter(id self, SEL _cmd){
    MKJAutoDictionary *dict = (MKJAutoDictionary *)self;
    return [dict.storeDict valueForKey:@"123"];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selName = NSStringFromSelector(sel);
    if ([selName containsString:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictSetter, "v@:@");
    }else{
        class_addMethod(self, sel, (IMP)autoDictGetter, "@@:");
    }
    return YES;
}

第二種形態 char *

這裏我把功能簡化了,只是演示一下如何動態給類添加方法,首先進入resolveInstanceMethod,然後根據需要調用class_addMethod方法,回顧下Method結構體,由SEL,IMP和簽名組成,那麼當我們動態添加的時候,根據參數就能看出class_addMethod第一個參數self就是Target,第二個參數就是SEL,第三個就是IMP,第四個就是我們所說的方法簽名,還記得這句話嗎? 他們可以在運行時用NSMethodSignature和C的char *來表示

NSInvocation

An Objective-C message rendered as an object.

把消息呈現爲對象形式。可以存儲消息的所有配置和直接調用給任意對象,這就是萬物皆對象的一種實踐了。
這個東西就是蘋果工程師提供的一個高層消息轉發系統。他是一個命令對象,可以給任意OC對象發送消息,那麼與之類似的還有一個performSelector,這裏咱們介紹NSInvocation,相比前者有他的短板

  1. ARC下可能會導致內存泄露
  2. performSelector最多接收兩個參數,如果參數多餘兩個 ,就需要組裝成字典類型了
  3. 他的參數類型限制爲id,如果用普通配型Int Double NSInteger爲參數的方法使用時會導致一些詭異的問題

步驟

使用這個類大致可以總結爲如下幾個步驟:

  1. 根據Selector來初始化方法簽名對象 NSMethodSignature
  2. 根據方法簽名對象來初始化NSInvocation對象,必須使用 invocationWithMethodSignature:方法
  3. 設置默認的TargetSelector
  4. 設置方法簽名對應的參數,從下標2開始,超出簽名參數index就越界報錯
  5. 調用NSInvocation對象的invoke方法
  6. 若有返回值,使用NSInvocationgetReturnValue 來獲取返回值,注意該方法僅僅就是把返回數據拷貝到提供的內存緩存區,並不會考慮這裏的內存管理

案例分析一(簡單Demo)

- (void)viewDidLoad {
    [super viewDidLoad];
	......
	NSString *name1 = @"Kimi貓";
    NSString *category1 = @"波斯貓";
    SEL targetSel = @selector(getCatAction:category:);
    NSMethodSignature *signature1 = [self methodSignatureForSelector:targetSel];
    NSInvocation *invocation1 = [NSInvocation invocationWithMethodSignature:signature1];
    [invocation1 setTarget:self];
    [invocation1 setSelector:targetSel];
    [invocation1 setArgument:&name1 atIndex:2];
    [invocation1 setArgument:&category1 atIndex:3];
    // 越界崩潰
    // [invocation1 setArgument:&category1 atIndex:4];
    [invocation1 invoke];
    
    // Error Code
    // Cat *cat1 = nil;
    // [invocation getReturnValue:&cat1];
    // NSLog(@"%@",cat1);

    // Plan A
     Cat *__unsafe_unretained cat1 = nil;
     [invocation1 getReturnValue:&cat1];
     Cat *finalCat = cat1;
    
    // Plan B
//    void *cat1 = NULL;
//    [invocation getReturnValue:&cat1];
//    Cat *finalCat = (__bridge Cat *)cat1;
 
    NSLog(@"Get Cat Instance Description---%@",finalCat);
 	......  
}
    
- (Cat *)getCatAction:(NSString *)name category:(NSString *)category{
    NSLog(@"%@--%@",self,NSStringFromSelector(_cmd));
    Cat *t = [[Cat alloc] init];
    t.name = name;
    t.category = category;
    return t;
}
https://developer.apple.com/documentation/foundation/nsinvocation/1437834-setargument?language=objc

這個Demo可以很好的反映出用法以及一些涉及到的坑。首先用法步驟最基本就是如此,這裏有幾個需要注意的點。

1.setArgument的下標是從2開始的,0存儲的是self,1存儲的是_cmd,而且參數需要傳入一個變量的指針或者內存地址,方便內部直接把數據寫入內存,而且signature1對象中有實際參數數量,如果超過數量就會越界崩潰

2.還是setArgument:atIndex:認不會強引用它的 argument,如果 argument 在 NSInvocation 執行的時候之前被釋放就會造成野指針異常(EXC_BAD_ACCESS),必要時加上[invocation retainArguments];即可

3.getReturnValue參數也是傳入變量的指針地址,這裏就有個很關鍵的東西,看上面的Error Code註釋那一坨,想當然,我們會這麼寫,但是這樣就崩潰了,而且報錯的是Bad Acess.....,這個作爲一個iOS開發菜鳥,可以斷定,顯然是內存泄漏了。但是怎麼看都沒看到泄漏呀,找到StackOverFlow的介紹
意思是該方法,不管類型是是, 它只負責把返回的數據複製到給定的內存緩衝區內。很顯然,它不關心內存管理。如果返回的是被對象指針類型引用,比如上面的Cat *cat1默認就是__strong類型的,ARC下,編譯器會接收內存管理的操作,因此,編譯器認爲它已經retain一次了,然後再後面補了一個release操作,在超出範圍的時候釋放掉,但是由於上面的是直接把返回值寫入內存,我纔不管你什麼內存管理,你編譯器自己自作多情認爲__strong修飾我已經在自己生產的setter方法retain一次了,很明顯不正確,所以這次加的release直接放內存泄漏了。 該段是個人理解,如果理解上有問題,歡迎在評論區指出

那麼咱們列舉了兩個解決方案PlanAPlanB,原理都是一樣的,就是getReturnValue是簡單的內存賦值,不會有任何內存管理,那麼我們也給這個對象修飾成讓編譯器認爲不需要retain的樣子,比如__unsafe_unretained__weakvoid *,之後再用其他變量來引用賦值即可。

這裏建議用PlanB模式,因爲getReturnValue本來就是給內存緩存區寫入數據,緩存區聲明爲void *更爲合理,然後通過__bridge的方式轉換爲OC對象類型把內存管理交給ARC,因此就有了下面的通用方案

案例分析(通用類型)

@implementation MKJInvocationManager

+ (BOOL)invokeTarget:(id)target
              action:(SEL)selector
           arguments:(NSArray *)arguments
         returnValue:(void* _Nullable)result{
    if (target && [target respondsToSelector:selector]) {
            NSMethodSignature *sig = [target methodSignatureForSelector:selector];
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
            [invocation setTarget:target];
            [invocation setSelector:selector];
            for (NSUInteger i = 0; i<[arguments count]; i++) {
                if (i >= (sig.numberOfArguments - 2)) {
                    break;
                }
                NSUInteger argIndex = i+2;
                id argument = arguments[i];
                if ([argument isKindOfClass:NSNumber.class]) {
                    //convert number object to basic num type if needs
                    BOOL shouldContinue = NO;
                    NSNumber *num = (NSNumber*)argument;
                    const char *type = [sig getArgumentTypeAtIndex:argIndex];
                    if (strcmp(type, @encode(BOOL)) == 0) {
                        BOOL rawNum = [num boolValue];
                        [invocation setArgument:&rawNum atIndex:argIndex];
                        shouldContinue = YES;
                    }
                    
                    /....此處省略NSNumber其他類型的判斷.../
                    
                    if (shouldContinue) {
                        continue;
                    }
                }
                if ([argument isKindOfClass:[NSNull class]]) {
                    argument = nil;
                }
                [invocation setArgument:&argument atIndex:argIndex];
            }
            [invocation invoke];
            NSString *methodReturnType = [NSString stringWithUTF8String:sig.methodReturnType];
            if (result && ![methodReturnType isEqualToString:@"v"]) { //if return type is not void
                if([methodReturnType isEqualToString:@"@"]) { //if it's kind of NSObject
                    // 初始化一個 const void * 類型
                    CFTypeRef cfResult = nil;
                    // 獲取值
                    [invocation getReturnValue:&cfResult];
                    if (cfResult) {
                        // 手動 retain一次
                        CFRetain(cfResult);
                        // 手動retain 才能在這裏不崩,如果沒有retain,那麼 __bridge_transfer 會先執行release
                        // “被轉換的變量”所持有的對象在變量賦值給“轉換目標變量”後隨之釋放  cfReslut 所以上面需要手動retain一次
                        id transferObj = (__bridge_transfer id)cfResult;
                        *(void **)result = (__bridge_retained void *)transferObj;
                        // const void *  Assigning to 'void *' from 'CFTypeRef' (aka 'const void *') discards qualifiers
                        // *(void**)result = cfResult;
                    }
                } else {
                    [invocation getReturnValue:result];
                }
            }
            return YES;
        }
        return NO;
}
@end

這段代碼就是通用類型了的處理了,首先處理了參數越界問題,又可以處理NSNumber類型的判斷,而且我們返回值接收的類型的是void *類型,如果不好理解可以把它理解爲我們經常使用的NSError * __autoreleasing *錯誤的捕獲。

看下核心[invocation invoke];之後的獲取返回值的操作,區分兩種類型,如果是普通數據類型,直接調用[invocation getReturnValue:result];即可,但是如果是對象類型,返回值不再是v的簽名而是@。裏面我們用到的類型是CFTypeRef進行取值,拿到的雖然是void *類型,甚至是const修飾的,正常情況直接*(void**)result = cfResult;就行了,但是類型有const修飾,會有警告,就有了上面的橋接轉換。我們要把cfResult轉換成id類型,用到了__bridge_transfer,改修飾符我上面也加了註釋,是把C轉換成OC對象,“被轉換的變量”所持有的對象在變量賦值給“轉換目標變量”後隨之釋放,會手動釋放一起,因此我們就需要在之前先執行CFRetain(cfResult),避免內存泄漏。後面再把id類型轉換成void *即可。

下面演示一下
定義一個類,直接通過我們寫的NSInvocation的方式調用

@interface Person : NSObject

- (instancetype)initWithVooVVideoManager:(void(^)(void))playBlock;

- (void)instanceSayHello;

@end

@implementation Person

- (instancetype)initWithVooVVideoManager:(void (^)(void))playBlock{
    self = [super init];
    if (self) {
        NSLog(@"Video初始化%@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
    }
    return self;
}

- (void)instanceSayHello{
    NSLog(@"Hello instance JX VV I am Back %@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
}

@end


// 在另一個類中觸發
void(^block)(void) = ^(void){
        NSLog(@"我丟");
    };
id obj = nil;
[MKJInvocationManager invokeTarget:[NSClassFromString(@"Person") alloc] action:@selector(initWithVooVVideoManager:) arguments:@[block] returnValue:&obj];
[MKJInvocationManager invokeTarget:obj action:@selector(instanceSayHello) arguments:nil returnValue:nil];

// 2020-05-19 17:28:45.896318+0800 YTKNetworkDemo[62992:1222263] Video初始化Person--initWithVooVVideoManager:
// 2020-05-19 17:28:45.896409+0800 YTKNetworkDemo[62992:1222263] Hello instance JX VV I am Back Person--instanceSayHello

可以看到,NSInvocation通過簽名初始化後,我們只要組合出這四個參數,就可以中轉所有的方法了
1、target
2、selector
3、arguments
4、return value

手動觸發消息轉發

先回顧下Runtime的消息轉發步驟。咱們姑且認爲有三次機會處理未被找到的消息。

1.resolveInstanceMethod:與resolveClassMethod:
該方法的實現我們再上面講NSMethodSignature的時候通過class_addMethod介紹了

2.forwardingTargetForSelector
這個更沒有什麼講的,就是指定一個新的Target轉發,只能轉給一個對象

3.forwardInvocation: 和 methodSignatureForSelector:
這個就厲害了,支持將消息轉發給任意多個對象,所以多繼承也只能採用forwardInvocation:的方式,由於咱們正好在講NSInvocation,就順便帶一個Demo來看看,多繼承這種能做,但沒必要,畢竟不常用。

模擬不再找不到消息而崩潰

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  [anInvocation invokeWithTarget:nil];
}

在這裏,調用invoke的時候,是可以傳nil的,畢竟給nil傳遞任何消息都會返回nil。

模擬下另一個手動觸發消息轉發,我們知道如果methodimp被指向__objc_msgForward,消息將直接進入轉發模式。下面我們假定上面的Person對象方法reloadData已經被檢測到崩潰了,因此我們需要手動替換調,一種方法是通過MethodSwizzle,咱們這裏用class_replace來模擬下

@interface Person : NSObject

- (instancetype)initWithVooVVideoManager:(void(^)(void))playBlock;

- (void)instanceSayHello;

- (void)reloadData;

@end


@implementation Person

- (instancetype)initWithVooVVideoManager:(void (^)(void))playBlock{
    self = [super init];
    if (self) {
        NSLog(@"Video初始化%@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
    }
    return self;
}

- (void)instanceSayHello{
    NSLog(@"Hello instance JX VV I am Back %@--%@",NSStringFromClass(self.class),NSStringFromSelector(_cmd));
}


- (void)reloadData{
    NSLog(@"Person ReloadData");
}

@end


void customForward(id self, SEL _cmd, NSInvocation *invo){
    if (invo.selector == @selector(instanceSayHello)) {
        NSLog(@"instanceSayHello  被完全轉發覆蓋");
    }else if (invo.selector == @selector(reloadData)){
        NSLog(@"reloadData  被完全轉發覆蓋");
    }
}

- (void)manualForwaringMesage{
    class_replaceMethod(NSClassFromString(@"Person"), @selector(instanceSayHello), _objc_msgForward, "v@:");
    class_replaceMethod(NSClassFromString(@"Person"), @selector(reloadData), _objc_msgForward, "v@:");
    class_replaceMethod(NSClassFromString(@"Person"), @selector(forwardInvocation:), (IMP)customForward, "v@:@");
//    [MKJInvocationManager invokeTarget:[NSClassFromString(@"Person") alloc] action:@selector(instanceSayHello) arguments:nil returnValue:nil];
    Person *p = [Person new];
    [p instanceSayHello];
    [p reloadData];
}

// 2020-05-19 18:11:38.195088+0800 YTKNetworkDemo[25388:1340307] instanceSayHello  被完全轉發覆蓋
// 2020-05-19 18:11:38.195437+0800 YTKNetworkDemo[25388:1340307] reloadData  被完全轉發覆蓋

可以看到instanceSayHello方法直接跳到了自定義的customForward上面,首先通過class_replaceMethod覆蓋掉instanceSayHello的實現爲_objc_msgForward,直接進入消息轉發,有三個步驟,上面說了,我們不做處理,直接來到forwardInvocation,因爲該方法已經被replace掉了,替換成了我們自己的customForward,攜帶了最終的NSInvocation信息,然後我們可以在自定義的方法中實現自己的邏輯,完成轉發覆蓋。上述只是一個簡單的例子,如果自定義的函數里根據每個invocationSEL名字動態化新建一個包含完整代碼完全不同的invocation,功能將會異常強大。實際上JSPatch的某些核心部分也正是使用了這種方式直接替換掉某些類裏的方法實現。

總結

簡單聊了下NSInvocationNSMethodSignature,重新回顧後,發現理解起來更容易了。我記得之前做組件化的時候看到CasaCTMediator方案,中介者模式好像是用performSelector的方法實現的,或許也可以用NSInvocation來嘗試下,各位大佬如果看完了,看到有哪些觀點有問題的,歡迎在評論區留言指正。

參考文章:
performSelector內存泄漏
typeEncoding
方法編碼
消息轉發和消息發送
NSINvocation return Value EXC_BAD_ACCESS
名詞介紹
Log Message Send打印
同上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章