Block从入门到放弃

417 阅读6分钟

Blocks是带有自动变量(局部变量)的匿名函数,能够捕获它所在函数内部的变量。实质是OC对闭包的对象实现,是一种特殊的数据类型,其可以作为变量被定义、可以作为参数、可以作为返回值,block的声明与赋值只是保存了一段代码段,在调用时执行内部的代码。block的设计思想是可以将有意义的代码片段组群成一个区块(block),而非转成分散且特定命名的程序。区块可以有区块外部无法通过名称访问,属于区块本身的变量、程序以及函数。

官方解释:

Block对象是C级语法和运行时功能。 它们类似于标准C函数,但除了可执行代码之外,它们还可能包含对自动(堆栈)或托管(堆)内存的变量绑定。 因此,块可以维护一组状态(数据),它可以用于在执行时影响行为。您可以使用块来组合可以传递给API的函数表达式,可选地存储并由多个线程使用。 块作为回调特别有用,因为块包含要在回调时执行的代码和执行期间所需的数据。

一.Block的模式和实质

Obejctive-C中,Block实际有自己的isa指针,它是被当做一个对象处理的,调用的时候也是给这个block发送了一个消息。block在OC中的实现如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...); /* 函数指针,指向block的实现代码 */
    struct Block_descriptor *descriptor; /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved; /*今后升级版本所需区域*/
    unsigned long int size; /*block的大小*/
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

在Block结构体中isa常见的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock这3种,它表示当前的block位于不同的存储区域。

分别表示为:

  • _NSConcreteStackBlock表示存储域在栈上,复制效果为从栈复制到堆。
  • _NSConcreteGlobalBlock表示程序的数据区域,复制效果什么也不做。
  • _NSConcreteMallocBlock表示存储域在堆上,复制效果引用计数增加。

 在实际应用中经常使用某种相同类型的block,可以通过使用typedef来抽象出这种类型的block。例如:

typedef void (^RequestFinishedBlock)(RequestSetup *setup, id components);

+ (NSString *)requestWithSetup:(RequestSetup *)setup
                       success:(RequestFinishedBlock)successBlock
                          fail:(RequestFailedBlock)failBlock
                        cancel:(RequestCancelBlock)cancelBlock;

Block捕获外部变量和对象的实质

之前我们说block是“带有自动变量(局部变量)的匿名函数”,那么“带有自动变量”在block中的变现即为捕获外部变量。例如下面这段代码:

int main()
{
    int a = 10;
    const char *fmt = "val = %d/n";
    void (^blockTest)(void) = ^{
        printf(fmt, a);
    };
    
    a = 2;
    fmt = "Changed Value = %d/n";
    
    blockTest();
    return 0;
}

上面代码的执行结果是 val = 10

这是因为block在定义的时候便将局部变量的值传给block变量所指向的结构体,即将局部变量的值传递给结构体的构造函数进行保存,同时因为捕获了局部变量,block的体积也会变大。捕获的变量是值传递而不是指针传递,也就是说Block仅仅截获自动变量的值,所以在调用block之前对局部变量进行修改并不会影响block内部的值,同时内部的值也是不可修改的。

将值赋值给block捕获的自动变量是不可以的,在编译时会产生编译错误。但是调用变更捕获变量的方法是完全没有问题的。例如:

    id array = [NSMutableArray array];
    void (^blockTest)(void) = ^{
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };

这段代码是可以编译通过的。

在OC中,C结构体里不能含有被__strong修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体。但是OC的运行时库能够准确把握Block从栈复制到堆,以及堆上的block被废弃的时机,在实现上是通过__main_block_copy_0函数和__main_block_dispose_0函数进行的,从字面上就可以了解,从栈复制到堆时,调用__main_block_copy_0函数。堆上的block被废弃时,调用__main_block_dispose_0函数。block在下面这些情况会从栈复制到堆:

  • 向方法或函数的参数中传递block时
  • Cocoa框架的方法且方法名中含有usingBlock时
  • Grand Central Dispatch API
  • block调用copy函数时

那么如果要在block中修改捕获的变量的值应该怎么办呢?解决这个问题有2种方法

第一种,C语言中有一个变量,允许block改写值。分别为静态变量、静态全局变量、全局变量。可以理解为block可以改变存储于特殊存储区域的变量。因为在block中全局变量和全局静态变量没有被截获到block里面,它们的访问是不经过block的,可直接使用。访问静态变量时,将静态变量的指针传递给block结构体的构造函数并保存。这是超出作用域使用变量的最简单方法。但是为什么捕获的自动变量不能这么做呢?因为变量作用域结束的同时,原来的自动变量被废弃,block中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。

第二种,使用“_block”修饰符。简单的来说就是当_block修饰自动变量时,会改变这个自动变量的存储区域。当block外的变量被_block修饰时,此变量会生成一个结构体。如下:

struct __Block_byref_val_0 {
   void *__isa;
   __Block_byref_val_0 *__forwarding;
   int __flags;
   int __size;
   int val;
};

且在block内部会隐性的增加一个成员变量,它是一个结构体指针,指向如上这个结构体。

在上面结构体中,int val-保存了最初的val变量,也就是说原来单纯的int类型的val变量被__block修饰后生成了一个结构体。这个结构体其中一个成员变量持有原来的val变量。__forwarding-通过__forwarding,可以实现无论__block变量配置在栈上还是堆上都能正确地访问__block变量,也就是说__forwarding是指向自身的。

还有一点不同是,在MRC环境下,_block根本不会对指针所指向的对象执行copy操作,而只是把指针进行复制。而在ARC环境下,对于声明为_block的外部对象,在block内部会进行retain操作,以至于在block环境内能安全的引用外部对象,以至于产生循环引用问题。

二.Block的循环引用

如果在Block内部使用__strong修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有。所以如果这个对象还同时持有Block的话,就容易发生循环引用。在这个时候对引用对象使用_weak修饰符即可。