开发做久了,就会感觉每天的业务开发都是重复性的工作,不管你承认或不承认,刚刚开始接触编程这份事业时的好奇心、灵动性都略有下降,说到”下降”这显然是不科学的,只不过是重复性工作让你的这些特质隐藏,它其实并没有消失反而是比之前更加显著,随着知识储备的递增,同样问题的解法不再局限于单一思维,权且当做一个游戏,就可以大开脑洞地提出更加新颖的思路。本文就sunnyxx关于block调用的一次分享,来唤起你对编程的新的欲望与兴趣。

首先,问题是这样的:调用一个void (^)(void)类型的block,不可以使用block()。 这里我们假设这个block是这样定义的:

void (^block)(void) = ^{
    NSLog(@"haha, block被调用了");
};

思路一

我们稍作分析,void (^)(void)类型的block可谓是最简单的block了,而在开发中使用的众多库都定义了这个类型的block,如

// libDispatch
typedef void (^dispatch_block_t)(void);
// libOS
typedef void (^os_block_t)(void);

这里很容易联想到使用GCD方法对block进行调用,如下面两个方法都可以

dispatch_async(dispatch_get_main_queue(), block);
    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), block);

同样的思路,iOS SDK中也有许多将block当做方法参数传递并执行的方法,如UIView的动画执行方法、NSRunLoop执行block等,因此可以像下面这样调用:

[UIView animateWithDuration:0 animations:block];

[[NSRunLoop currentRunLoop] performBlock:block];

总结一下思路一:利用C库和iOS SDK中的函数或方法直接调用

思路二

这里的思路二是对思路一的延伸,同样是利用系统提供好的函数或方法,但是这些方法不会自动执行block,而是程序员手动通过对包装类型的方法调用来执行:

[[NSBlockOperation blockOperationWithBlock:block] start];

[[[NSThread alloc] initWithBlock:block] start];

以上的两个方法都是利用这个思路解题。

总结一下思路一和思路二:都是对系统已有方法的调用,具体的方案体现了开发者对API的熟悉程度。

思路三

NSInvocation可以设置target为一个block,这是NSInvocation类的一个隐藏功能,我翻阅许多资料并没有找到有关Invocation如何区别处理一般对象类型和block类型,反倒是大家都在疯狂讨论如何使用这一隐藏功能。(如果你找到了NSInvocation对不同类型target区别处理的相关文档,当然最好是苹果的文档,希望可以在下面留言告知,不胜感激。)

根据使用的情况,可以反推断NSInvocation的target为一般对象类型和block类型,有以下几点不同:

  普通对象 block
方法类型签名
(method signature)
可以通过[obj methodSignatureForSelector]创建 必须要手动拼装,可以手写或者通过@encode()计算,最后使用[NSMethodSignature signatureWithObjCTypes]创建
方法类型签名字符特点 第二三个字符固定为”@:”, 代表对象类型和selector 第二三个字符固定为”@?”,代表对象类型,和一个未知类型,一般认为这两个组合就代表着block,@encode(任意block类型) == “@?”
selector 必须指定 不指定
输入参数 Index从2开始 Index从1开始
invoke调用 是对obj指定selector的调用 是对block本身的调用

基于对block处理的隐藏特性,我们可以使用这样的方法调用block

NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation invokeWithTarget:block];

这里使用了直接手写签名encode type的方式,针对复杂参数的block,应该使用@encode进行拼装,这个方法的使用不仅仅局限于void (^)(void)类型的block,它适用于各种类型的block。

思路三的实现主要是基于对NSInvocation处理target为block这一特殊机制的了解。

思路四

block本身是一个包装的对象类型,我们可以使用DLIntrospection这个内省工具查看它的继承关系,可以查看到__NSMallocBlock____NSGlobalBlock__这两种block类型的继承关系

__NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject

__NSGlobalBlock__ -> __NSGlobalBlock -> NSBlock -> NSObject

进一步得到NSBlock的对象方法列表:

po [[NSBlock class] instanceMethods]

- (id)copy,
- (id)copyWithZone:({_NSZone=} *)arg0 ,
- (void)invoke,
- (void)performAfterDelay:(double)arg0

可以推测,注意只是推测,block的调用实际上就是调用自身的invoke方法或者是performAfterDelay:方法,于是得到这样的调用方案:

[block invoke];

[(id)block performSelector:NSSelectorFromString(@"performAfterDelay:") withObject:@(0)];

思路四的实现是基于对block包装类型(__NSMallocBlock__, __NSGlobalBlock__, __NSStackBlock__)的理解和面向对象思想的扩展。

思路五

libclosure(block的实现)源码中,有一个描述block内存布局的结构体:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

这里就能轻易地发现一个函数指针invoke,可以猜测block的调用实际上就是对这个函数指针的调用,甚至可以推测NSBlockinvoke方法实现应该就对这个函数指针调用。于是这里有了一个调用这个指针指向的函数的方案:

void *pBlock = (__bridge void *)(block); // 首地址
void **pArray = (void **)pBlock; // 转为(void *)数组
// printf("%zd", sizeof(void *)); // 8 (64bit环境下)
// 8 + 4 + 4 = 8 * 2
void (*invoke)(void *, ...) = *(pArray + 2); // 或者pArray[2]
invoke(pBlock);

思路五主要是基于对block源码的理解。

思路六

__attribute__是GNU C的一大特性,可以为变量或者方法等设置一些编译器属性,如果不了解它是什么可以查看__attribute__。这里介绍一个特殊的变量属性:__attribute__((cleanup)):这个attribute的功能是,被它修饰的变量出了作用域时,自动调用这个attribute指定的函数来做清理工作,而这个函数的第一个参数是指向这个变量的指针。

可以在自动调用的这个函数里完成对block的调用,具体实现为:

static void blockCleanUp(__strong void (^*block)(void)) {
    (*block)();
}

- (void)viewDidLoad {
    [super viewDidLoad];

    void(^block)(void) = ^{
        NSLog(@"haha, block被调用了");
    };

    {
        __strong void(^cleaner)(void) __attribute__ ((cleanup(blockCleanUp), unused)) = block;
    }
}

思路六的实现在于利用一个可以自动调用方法的编译器特性为媒介实现对传入block的调用。而实际上是它的实现无异于思路一中提出的方案,反而又比第一种麻烦许多,因为自动调用的方法还要自己实现。这里权当学习一下__attribute__((cleanup))这个attribute。

思路七

使用汇编。提起汇编,把许多人吓着了,可是不懂汇编呀,但是知道每条代码都会对应着多条汇编指令,调用block比不调用block多出来的汇编代码不就是调用block的汇编代码嘛。其实我们可以按照这样的思路来偷取Xcode的汇编代码:先不调用block,加入断点,查看汇编代码(这里使用模拟器测试):

- (void)viewDidLoad {
    [super viewDidLoad];

    void(^block)(void) = ^{
        NSLog(@"haha, block被调用了");
    };
    // 这里插入断点
}

在断点处,获取汇编指令。有两种方法:①使用lldb指令dis打印汇编指令,②编辑视图左上角的图标,选择反汇编(Disassembly)。

然后在调用block的情况下获取汇编代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    void(^block)(void) = ^{
        NSLog(@"haha, block被调用了");
    };
    block();
    // 这里插入断点
}

对比一下两段汇编代码: 两段汇编代码对比 然后将block();替换为汇编代码就可以了:

- (void)viewDidLoad {
    [super viewDidLoad];

    void(^block)(void) = ^{
        NSLog(@"haha, block被调用了");
    };
    asm("movq   %rax, -0x28(%rbp)");
    asm("movq   -0x28(%rbp), %rax");
    asm("movq   %rax, %rsi");
    asm("movq   %rsi, %rdi");
    asm("callq  *0x10(%rax)");
}

思路七的实现是基于等效代码替换,或者说是用底层代码对上层代码替换。