1. 程式人生 > >iOS performSelector多引數傳遞解決方案以及objc_msgSend的使用注意事項

iOS performSelector多引數傳遞解決方案以及objc_msgSend的使用注意事項

iOS performSelector多引數傳遞解決方案


以及objc_msgSend的使用注意事項



iOS中使用performSelector:withObject:withObject:方法最多傳遞兩個引數

    [self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]

解決方案:1. 使用NSInvocation進行訊息轉發從而實現對performSelector的多引數傳遞

               2. 使用runtime中的objc_msgSend進行訊息的傳送


方案一:

以下是對NSObject類的擴充套件方法:

NS_REQUIRES_NIL_TERMINATION:是對多引數傳遞值得一個巨集

va_list args:定義一個指向個數可變的引數列表指標;

va_start(args,object):object為第一個引數,也就是最右邊的已知引數,這裡就是獲取第一個可選引數的地址.使引數列表指標指向函式引數列表中的第一個可選引數,函式引數列表中引數在記憶體中的順序與函式宣告時的順序是一致的。


va_arg(args,id):返回引數列表中指標所指的引數,返回型別為id,並使引數指標指向引數列表中下一個引數。

va_end(args):清空引數列表,並置引數指標args無效。

其他說明見程式碼註釋

-(id)glt_performSelector:(SEL)selector withObject:(id)object,...NS_REQUIRES_NIL_TERMINATION;
{
    //根據類名以及SEL 獲取方法簽名的例項
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
    if (signature == nil) {
        NSLog(@"--- 使用例項方法呼叫 為nil ---");
        signature = [self methodSignatureForSelector:selector];
        if (signature == nil) {
            NSLog(@"使用類方法呼叫 也為nil, 此時return");
            return nil;
        }
    }
    //NSInvocation是一個訊息呼叫類,它包含了所有OC訊息的成分:target、selector、引數以及返回值。
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    NSUInteger argCount = signature.numberOfArguments;
    // 引數必須從第2個索引開始,因為前兩個已經被target和selector使用
    argCount = argCount > 2 ? argCount - 2 : 0;
    NSMutableArray *objs = [NSMutableArray arrayWithCapacity:0];
    if (object) {
        [objs addObject:object];
        va_list args;
        va_start(args, object);
        while ((object = va_arg(args, id))){
            [objs addObject:object];
        }
        va_end(args);
    }
    if (objs.count != argCount){
        NSLog(@"--- objs.count != argCount! please check it! ---");
        return nil;
    }
    //設定引數列表
    for (NSInteger i = 0; i < objs.count; i++) {
        id obj = objs[i];
        if ([obj isKindOfClass:[NSNull class]]) {
            continue;
        }
        [invocation setArgument:&obj atIndex:i+2];
    }
    [invocation invoke];
    //獲取返回值
    id returnValue = nil;
    if (signature.methodReturnLength != 0 && signature.methodReturnLength) {
        [invocation getReturnValue:&signature];
    }
    return returnValue;
}



注意:在Swift中沒有NSMethodSignature和NSInvocation方法,暫時只能通過橋接解決,Swift中給出了這樣一句:

NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
如果在Swift中誰有好的方法可以擴充套件,請求大神告知!不勝感激!

方案二:

objc_msgSend :

objc_msgSend(<#id self#>, <#SEL op, ...#>)

概念:其實每個類中都有一張方法列表去儲存這個類中有的方法,當發出objc_msgSend方法時候,就會順著列表去找這個方法是否存在,如果不存在,則向該類的父類繼續查詢,直到找到位置。如果始終沒有找到方法,那麼就會進入到訊息轉發機制;objc_msgSend被分為2個過程:1)在cache中尋找SEL。2)在MethodTable尋找SEL。

具體使用介紹:

在一個列中有以下兩個方法:

@implementation CustomClass

-(void)fun
{
    NSLog(@"fun");
}

-(void)eat:(NSString *)food say:(NSString *)some
{
    NSLog(@"%@ %@",food, some);
}

@end

當我們使用的時候,使用objc_msgSend進行呼叫,如下:這樣便呼叫了類中的fun方法

TestClass *cls = [[TestClass alloc] init];

objc_msgSend(cls, @selector(fun)); //錯誤寫法(arm64崩潰偶爾發生)

((void (*)(id, SEL))objc_msgSend)(cls, @selector(fun)); //正確寫法

//具體原因見下面解釋 objc_msgSend arm64 崩潰問題


使用注意1:使用objc_msgSend crash解決方案



使用注意2:objc_msgSend arm64 崩潰問題
之前一直用objc_msgSend,但是沒注意apple的文件提示,所以突然objc_msgSend crash了。
按照文件 64-Bit Transition Guide for Cocoa Touch 給出了以下程式碼片段:

- (int) doSomething:(int) x { ... }
- (void) doSomethingElse {
    int (*action)(id, SEL, int) = (int (*)(id, SEL, int)) objc_msgSend;
    action(self, @selector(doSomething:), 0);
}
所以必須先定義原型才可以使用,這樣才不會發生崩潰,呼叫的時候則如下:

void (*glt_msgsend)(id, SEL, NSString *, NSString *) = (void (*)(id, SEL, NSString *, NSString *))objc_msgSend;

glt_msgsend(cls, @selector(eat:say:), @"123", @"456");