Objective-C runtime 拾遺 (一)——NSInvocation 調用Block

一日在開發之中,遇到這樣一個問題,在某些場合,須要用NSInvocation來調用Block,而Block簽名並非固定,即,Block參數類型個數能夠不一樣。html

問題

回憶NSInvocation 通常用法

天然想到了NSInvocation,譬如以下代碼:ios

NSString* string = @"Hello";
NSString* anotherString = [string stringByAppendingString:@" World!"];

寫成Invocataion大體是這樣的:git

NSString* string = @"Hello";
NSString* anotherString;
NSString* stringToAppend = @" World!";
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:[NSString instanceMethodSignatureForSelector:@selector(stringByAppendingString:)]];
inv.target = string;
[inv setArgument:&stringToAppend atIndex:2];
[inv invoke];
[inv getReturnValue:&anotherString];

具體就不詳細介紹了,文檔裏講得很詳細。請移步Apple Docgithub

MethodSignature

一個問題是如何Block得到MethodSignature。Block沒有selector,但發現NSMethodSignature有這樣一個方法-[NSMethodSignature signatureWithObjCTypes:],那問題轉化成如何從Block得到編碼的Signature。objective-c

一搜索。發現Clang官方文檔stackoverflow都有說這個問題。(Clang官方文檔真是個寶庫啊)。shell

按Clang的文檔,Block定義以下:編程

struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 {
    unsigned long int reserved;     // NULL
    unsigned long int size;         // sizeof(struct Block_literal_1)
    // optional helper functions
    // void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
    // void (*dispose_helper)(void *src);             // IFF (1<<25)
    // required ABI.2010.3.16
    // const char *signature;                         // IFF (1<<30)
    void* rest[1];
    } *descriptor;
    // imported variables
};

中間註釋部分是對作了些小改造,由於對於能夠copy的Block,上述兩個函數指針才存在。(另外,發現其實Block還能經過block->invoke(...)來調用,先按下不表)。segmentfault

static const char *__BlockSignature__(id blockObj)
{
    struct Block_literal_1 *block = (__bridge void *)blockObj;
    struct Block_descriptor_1 *descriptor = block->descriptor;
    int copyDisposeFlag = 1 << 25;
    int signatureFlag = 1 << 30;
    assert(block->flags & signatureFlag);
    int offset = 0;
    if(block->flags & copyDisposeFlag)
        offset += 2;
    return (const char*)(descriptor->rest[offset]);
}

NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:__BlockSignature__(block)]];

最重要的一個問題解決了。以後就是對invocation調用setArgument,進行參數傳遞。(先簡化一下問題,參數都是NSObject,放在NSArray裏,關於參數獲取後面還有坑,之後再寫)app

for(NSUInteger i = 0; i < args.count ; ++i){
        id object = args[i];
        [invocation setArgument:&object atIndex:i + 2];
    }
    [invocation invokeWithTarget:block];

調用,Crash!越界了函數

Reason: -[NSInvocation setArgument:atIndex:]: index (5) out of bounds [-1, 4]

文檔是這樣描述setArgument

Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; these values can be retrieved directly with the target and selector methods. Use indices 2 and greater for the arguments normally passed in a message.

其實上述代碼問題不少,剛纔有點撞大運編程

  1. selector(即_cmd)哪裏去了?並無傳遞給NSInvocation

  2. 爲何越界,按照文檔說法應該從2開始。

  3. 爲何從-1開始

文檔說的言之鑿鑿,第一個參數傳self,第二個是selector(即_cmd),但block調用並無selector,參數個數其實能夠從MethodSignature獲取:invocation.methodSignature.numberOfArguments

因此這就是會越界的緣由,正確的作法是從1開始:

[invocation setArgument:&object atIndex:i + 1]

一調試,果真。

另外從-1開始緣由是-1的位置是存儲return result,固然這個結論我查了文檔並無找到,也是試出來的。囧。

源碼及其餘

源碼我放在了github,戳這裏

用法也很簡單:

NSInvocation* inv = [NSInvocation invocationWithBlock:block];

後續會增長一些接口如:
+ (instancetype) invocationWithBlockAndArguments:(id) block ,...;

更新

恩 已經增長了。

增長的接口用法:
對於

void (^myBlock)(id, NSArray*,double, int**) = ^(id obj1, NSArray* array, double dNum,int** pi) {
  NSLog(@"%@",@"Hey!");
};
int* i = NULL;
NSInvocation* inv = [NSInvocation invocationWithBlockAndArguments:myBlock,[NSObject new],@[@1,@2,@3],1.23,&i];

參數支持id,全部簡單值類型,IMP,SEL,Class,Block,指針, 但Struct,Union,C-style Array 不支持,比較預想的tricky,研究中。

原做寫於segmentfault 連接

相關文章
相關標籤/搜索