在 Objective-C 中有些函数可以接受可变的参数,比如常用的日志输出 NSLog,它的原型如下:
FOUNDATION_EXPORT void NSLog(NSString *format, ...);
苹果官方有一个类似的例子来解析其内部实现。
既然函数可以接受可变的参数,那 Block 是否也能实现这一点呢?
其实只要在 Block 声明的时候参数列表填空,该类型的 Block 就可以接收任意个数和任意类型的参数了。
typedef void (^SFCommonBlock)();
使用方法:
void (^block1)(void) = ^{
NSLog(@"Empty Block");
};
void (^block2)(NSInteger) = ^(NSInteger code){
NSLog(@"code = %ld", code);
};
void (^block3)(id, NSInteger) = ^(id response, NSInteger code) {
NSLog(@"response = %@, code = %ld", response, code);
};
SFCommonBlock commonBlock1 = block1;
SFCommonBlock commonBlock2 = block2;
SFCommonBlock commonBlock3 = block3;
commonBlock1(); // Empty Block
commonBlock2(999);// Code = 999
commonBlock3(@"Apple", 1984);// Response = Apple, Code = 1984
我们可以看到 commonBlock1,commonBlock2,commonBlock3,都是 SFCommonBlock 类型,可以传入不同类型和个数的参数。然而这样用看起来并没什么意义,还不如直接调用原来的 Block。
不过如果将 Block 作为参数传递的时候就有意义了,例如:
- (void)requestWithNumberOfArguments:(NSUInteger)count responseBlock:(SFCommonBlock)block {
switch (count) {
case 0: {
block();
} break;
case 1: {
block(999);
} break;
case 2: {
block(@"Response", 1000);
} break;
default:
break;
}
}
调用方只要传入 Block 参数的个数,就可以通过判断来执行相应的 Block。然而有一个不足之处是,调用时就没有代码提示了,需要手动挨个填写参数。
还有一个不是很优雅的地方是,每次调用的时候必须传递参数个数。能否有什么方法直接从传入的 Block 里面获取到参数的个数信息呢?
Objective-C 中方法调用实则是在 Runtime 期间消息的发送,每个对象能够接受的消息都会对应一个方法签名。方法签名中包含:参数的个数,返回值的类型,以及执行是异步的还是同步的。
如果我们能够拿到 Block 执行的方法签名,那么就可以知道 Block 参数的个数了。然而 Block 虽然是一个 Objective-C 的对象,但是并没有与之相关的公开 API,所幸 Runtime 的是开源的,可以在这里看到 Block 被编译后的结构:Block Implementation Specification
Block 的原型对应的是结构体,虽然我们无法直接进行访问,但我们可以自己撸一个跟原型一模一样的结构体,并将 block 实例强指过去,再通过这个结构体实例访问到 Block 的签名。结构体的原型是这个样子:
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct block_descriptor {
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)
} *descriptor;
// imported variables
};
enum {
BLOCK_HAS_COPY_DISPOSE = (1 << 25),
BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code
BLOCK_IS_GLOBAL = (1 << 28),
BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30),
};
在 block_descriptor 结构体中可以看到 signature 结构体。想必这就是 Block 的方法签名了,可以用这个 const char* 指针来初始化 NSMethodSignature 对象。
至于如何将 Block 映射到结构体上,有前辈已经实现过了,传送门。将指向 Block 的结构体的指针偏移指向 signature 字段,就可以取出签名字符串,进而初始化 NSMethodSignature 实例,使用代码如下:
- (void)responseBlock:(SFCommonBlock)block {
CTBlockDescription *blockDescription = [[CTBlockDescription alloc] initWithBlock: block];
NSMethodSignature * methodSignature = blockDescription.blockSignature;
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
switch (numberOfArguments - 1) { // 减去 block 对象自身
case 0: {
block();
} break;
case 1: {
block(999);
} break;
case 2: {
block(@"Response", 1000);
} break;
default:
break;
}
}
CTBlockDescription 是通过传入的 block 初始化出来的一个 Block 描述对象。其中有一个属性 blockSignature 就是 Block 的签名。
参考
非主流代码技巧
NSInvocation动态调用任意block
Block Implementation Specification
网友评论