美文网首页
理解“块”(blocks)这一概念

理解“块”(blocks)这一概念

作者: WhistleCai | 来源:发表于2018-11-15 11:07 被阅读0次

    block提供了闭包功能,这一语言特性作为一个扩展被添加到GCC编译器中,存在于所有现代Clang版本中(这个编译器工程被Mac OS X和iOS开发所使用)。block所需的运行时组件在Mac OS X 10.4和iOS 4.0和之后的所有版本中都可用.由于这个语言特性属于C语言级别的特性,因此在C,C++,Objective-C和Objective-C++代码中都可用,这些代码在一个支持该特性的编译器下编译,并运行在block运行时中。

    block基础知识

    block和函数相似,只不过是定义在另一个函数里面,和定义它的函数共享同一个范围内的东西。block用“^”符号来表示,后面跟着一对花括号,花括号里面是实现代码。

    ^{
    // Block implementation here
    }

    block是一个数据类型,可以被赋值。与int,float或其他Objective-C对象一样,一个block也可以被赋值到一个变量,与其他类型的变量一样被使用。block类型的语义类似于函数指针。下面是一个无输入参数和返回参数的block例子:

    void (^someBlock)() = ^{
    // Block implementation here
    };

    下面是block的原型:

    NSString * ( ^ myBlock )( int );
    

    上面的代码是声明了一个block(^)原型,名字就叫做myBlock,携带一个int参数,返回只为NSString类型的指针。

    下面来看看block的定义:

    myBlock = ^( int number )
    {
    return [ NSString stringWithFormat: @"Passed number: %i", number ];
    };

    如上所示,将一个函数体赋值给了myBlock变量,其接收一个名为number的参数。该函数返回一个NSString对象。

    如果不把block赋值给变量的话,可以忽略掉block原型的声明,例如直接将block当做实参进行传递。如下所示:

     someFunction( ^ NSString * ( void ) { return @"hello, world" } );
    

    注意,上面这种情况必须声明返回值的类型——这里是返回NSString对象。

    将block当做形参来传递

    由于Objective-C是强制类型语言,所以作为函数参数的block也必须要指定返回值的类型,以及相关参数类型(如果需要的话)。

    • ( void )logBlock: ( NSString * ( ^ )( int ) )theBlock;
      block的强大之处在于:在声明它的范围内,所有变量都可以为其所捕获(capture)。也就是说,在此范围内的所有变量,在block里依然可用。默认情况下,block所捕获的任何变量在block里都不能修改,否则编译器会报错,除非将这样想要被修改的变量声明为__block修饰符。下面是一个block用来统计数组中小于2的数量的例子:
    NSArray *array = @[@0, @1, @2, @3, @4, @5];
    __block NSInteger count = 0;
    [array enumerateObjectsUsingBlock: ^(NSNumber *number, NSUInteger idx, BOOL *stop){
        if ([number compare:@2] == NSOrderedAscending) {
            count++;
        }
    }];
    // count = 2
    

    上面这段代码也展示了内联块(inline block)的用法。在Objective-C引入block之前,想要实现这样的功能只能通过传入函数指针和选择子(selector)的名称,但是这样做需要传入传出状态,经常通过一个“不透明的void指针“(an opaque void pointer)”来实现,从而导致添加额外的代码,也会令方法变得松散。而声明一个内联块,可把所有业务逻辑都放在一处。

    当block捕获到一个对象类型的变量时,它会隐式地保留(retain)该对象变量。系统在释放(release)这个block的时候,也会将该对象一并释放。block本身可看做一个对象,也与其他对象一样有引用计数。当最后一个指向block的引用被移除之后,block会被回收,回收时也会释放它所捕获的对象变量,以平衡捕获时所执行的保留操作。

    如果block被定义在一个类的实例方法中,则self变量跟该类的所有实例变量一样都可以被block访问。需要特别注意的的是,这些实例变量都可以被block修改,而无需用__block修饰符来声明。但是,如果一个实例变量被block捕获,无论是读还是写,self变量都会被隐式地捕获,因为实例变量跟self所指代的实例关联在一起。这样就会引起self变量被block所retain。这种情况下,如果block本身再被self所指代的实例retain的话,就会经常导致“保留环”(retain cycles)。

    block的内部结构

    一个block是一个对象,因为定义它的内存区域中的第一个变量就是一个指向一个Class对象指针,就是所谓的isa指针。这个内存区域的剩余部分包含维持block正常运行的各种信息。一个block对象的内存布局如下:

    表一:


    表一.png

    表二:


    表二.png

    布局中最重要的变量是invoke,它是指向block的实现代码的函数指针,这里的函数原型至少传入一个void *参数,代表block本身。

    descriptor是指向结构体的指针,该结构体中声明了block对象的大小,还声明了copy和dispose这两个辅助函数所对应的函数指针。copy和dispose辅助函数在拷贝和释放block对象时运行,其中会执行一些操作,例如前者会retain所捕获的对象,后者会将它们release。

    block会包含所有它捕获的变量的拷贝,存储在descriptor变量之后,并分配足够多的空间以存储这些对象变量。需要注意的是,这并不意味着对象本身被拷贝,而是只拷贝指向这些对象的指针变量。当block执行的时候,这些捕获的变量需要从内存区域中读取出来,这也是为什么需要向invoke函数传递block参数的原因。

    全局块,栈块和堆块

    根据Block在内存中的位置分为三种类型:NSGlobalBlock,NSStackBlock, NSMallocBlock,即分别为全局块、栈块和堆块。

    当定义block的时候,block的内存区域分配在栈中,即为栈块(stack block)。这就容易引起一些错误:

    void (^block)();
    if ( /* some condition */ ) {
        block = ^{
            NSLog(@"Block A");
        };
    } else {
        block = ^{
            NSLog(@"Block B");
        };
    }
    block();
    

    上面这段代码中,block是在if与else语句中定义的,当超过这个语句范围之后,这段内存区域就有可能被覆盖,从而导致意想不到的错误。

    解决这个问题的方法是拷贝block,就向block对象发送copy消息。这样就将block从栈(stack)内存中拷贝到了堆(heap)内存中,从而可以在定义该block对象的范围之外使用它。此外,当被拷贝到堆中后,block就变成一个具有引用计数的对象。之后再对它进行copy操作,并不会真的执行copy操作,而是只增加它的引用计数。如果不再使用堆块(heap block)的话,在ARC(atomatic reference count)下就会自动释放,在MRC(manual reference count)下需要显式调用release函数。当引用计数为0的时候,堆块就会和其他对象一样被释放。然而,栈块(stack block)不需要显式释放,因为栈内存本来就会自动回收。

    上面的代码加上两个copy方法的调用就会安全了,如下:

    void (^block)();
    if ( /* some condition */ ) {
        block = [^{
            NSLog(@"Block A");
        } copy];
    } else {
        block = [^{
            NSLog(@"Block B");
        } copy];
    }
    block();
    

    如果是手动管理引用计数(MRC)的话,上面的代码在用完块之后还需将其释放。

    除了栈块和堆块,另一种块类型是全局块(global block)。这种块不需要捕获任何状态(比如外围的变量等),运行时也无需状态来参与。全局块的全部内存区域在编译时就已经完全确定,因此全局块被声明在全局内存里,而不需要每次用到的时候在栈中创建。这种块,实际上相当于单例(singletons)。这是一种减少不必要工作量的优化策略,如果把如此简单的块当做复杂的块来处理的话就会在copy和dispose该块时执行不必要的操作。

    需要注意的是,不同于NSObjec的copy、retain、release操作:

    • Block_copy与copy等效,Block_release与release等效;

    • 对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;

    • NSGlobalBlock:retain、copy、release操作都无效;

    • NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],(补:在ARC中不用担心此问题,因为ARC中会默认将实例化的block拷贝到堆上)在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock类型对象。

    • 尽量不要对Block使用retain操作。

    block作为属性时应使用copy进行声明

    typedef void (^XYZSimpleBlock)(void);
    
    @interface XYZObject : NSObject
    @property (copy) XYZSimpleBlock blockProperty;
    @end
    

    block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。

    block是C、C++、Objective-C中的词法闭包。
    block可以接受参数,也可返回值。
    block可以分配在栈或堆上,也可以是全局的。分配在栈上的block可拷贝到堆里,这样就合标准的Objective-C对象一样具有引用计数了。

    相关文章

      网友评论

          本文标题:理解“块”(blocks)这一概念

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