美文网首页iOS Developer
Block 如何使用可变参数

Block 如何使用可变参数

作者: SmallflyBlog | 来源:发表于2016-11-25 16:16 被阅读981次

    在 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

    相关文章

      网友评论

        本文标题:Block 如何使用可变参数

        本文链接:https://www.haomeiwen.com/subject/emqwpttx.html