Objective-C runtime 拾遺 (一)——NSInvocation 呼叫Block

NO IMAGE

一日在開發之中,遇到這樣一個問題,在某些場合,需要用NSInvocation來呼叫Block,而Block簽名並不是固定,即,Block引數型別個數可以不同。

問題

回憶NSInvocation 一般用法

自然想到了NSInvocation,譬如如下程式碼:

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

寫成Invocataion大致是這樣的:

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 Doc

MethodSignature

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

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

按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(...)來呼叫,先按下不表)。

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裡,關於引數獲取後面還有坑,以後再寫)

    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 連結