说说 Objective-C 中的 block

2,085 阅读5分钟
原文链接: www.jianshu.com

Apple从OS X 10.4和iOS 4以后开始支持block,相对于delegate,block有很多便捷之处,使得代码更简洁,可读性更强。下面我们来了解一下block。

一、block的基础知识

block语法
我通过以下图来了解block的语法,图片来自这里


block语法结构图


下面是一个简单的block

NSInteger (^addBlock)(NSInteger a, NSInteger b) = ^(NSInteger a, NSInteger b){
    return a + b;
};

这段代码定义了一个名为addBlock的block。该bolck有两个入参a和b,返回值值为NSInteger类型。它的调用方法如下,看起来像C的函数调用。

NSInteger add = addBlock(2, 5); // add = 7

使用typedef定义block类型
以上可以通过typedef来定义block,以便阅读

typedef NSInteger (^addBlock)(NSInteger a, NSInteger b);

在定义某个block类型时,可以使用

    addBlock aBlock = ^ (NSInteger a, NSInteger b) {
        //Implemention
    };

这样看起来,要比之前简单得多。

block的强大
block内可以访问block之前定义的变量:

NSInteger additional = 5;
NSInteger (^addBlock)(NSInteger a, NSInteger b) = ^(NSInteger a, NSInteger b) {
    return a + b + additional;
};   
NSInteger add = addBlock(2, 5); // add = 12

在block中改变block外定义的变量
想要在block中改变block外定义的变量的话,需要将该变量使用__block修饰:

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

以上代码用block的方法判断array中有多少个大于2的数,通过__block的修饰,在block内改变了count的值。
对于static变量,同样适用

NSArray *array = @[@0,@1,@2,@3,@4,@5];
static NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) {
        if ([number compare:@2] == NSOrderedAscending) {
            count++;
        }
}]; 
//count = 2

block可以访问类的实例变量和self变量

@interface EOCClass : NSObject 
@property (nonatomic, copy) NSString *anInstanceVariable;
@end

@implementation EOCClass

- (void)anInstanceMethod {

    void (^someBlock)() = ^ {
        self.anInstanceVariable = @"Something";
    };
    someBlock();
    NSLog(@"self.aninstanceVaraible = %@", self.anInstanceVariable);
    //self.aninstanceVaraible = Something
}

@end

block的内部结构

block 的数据结构定义如下(图片来自 这里):


block内存布局



对应的结构体定义如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

从上面代码看出,一个 block 实例实际上由 6 部分构成:

  • isa指针:指向该block类型的类的指针。
  • flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
  • reserved:保留变量,我的理解是表示block内部的变量数。
  • invoke:函数指针,指向block的实现代码地址。
  • descriptor:指向结构体的指针,block的附加描述信息,比如保留变量数、block的大小、copy和dispose辅助函数的函数指针指针,copy为保留捕获的对象,dispose为block释放时释放捕获的对象。
  • variables:因为block有闭包性,所以可以访问block外部的局部变量。这里为block捕获所有对象的指针。

block的类型

block有全局block、栈block以及堆block三种类型

  1. 栈block(NSConcreteStackBlock)
    定义块时,其所占的内存区域是分配在栈中的。也就是说,块只在定义它的那个范围内(作用域)内有效。如下面代码:
     void (^block)();
     if (/* DISABLES CODE */) {
         block = ^ {
             NSLog(@"Block A");
         };
     } else {
         block = ^{
             NSLog(@"Block B");
         };
     }
     block();
    以上代码,定义在if和else中的两个block都分配在栈内存中。编译器会给每个block分配好栈内存,然而等离开了相应的范围(对应于if和else的内的作用域)后,编译器有可能把分配给block的内存覆写掉。所以这两个block只能在保证在自己对应的if和else中的作用域有效。这样的代码在编译器没有覆写其block对应的内存的话,运行是正常的,如果被覆写了,则会crash。
    为了解决此问题,应用对改block进行copy操作,将其复制到堆上。

  2. 堆block(NSConcreteMallocBlock)
    想要将block复制到堆上,我们对block进行copy操作即可。如下
     void (^block)();
     if (/* DISABLES CODE */) {
         block = [^ {
             NSLog(@"Block A");
         } copy];
     } else {
         block = [^{
             NSLog(@"Block B");
         } copy];
     }
     block();
    拷贝到堆后,block的生命周期就与一般的OC对象一样了,在ARC下,我们通过引用计数来对其进行内存管理。
  3. 全局block(NSConcreteGlobalBlock)
    全局block,它不会捕捉任何状态(比如外围的变量等),运行时也无需有状态来参与。block所使用的整个内存区域,在编译期已经完全确定了,因此,全局block可以声明在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局block的copy操作是个空操作,因为全局block绝不可能为系统所回收。这种块实际上相当于单例。如下:
     void (^block)() = ^ {
       NSLog(@"This is a block);
     };
    该block所需要的全部信息都能在编译期确定。
  4. ARC 对 block 类型的影响
    在 ARC下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
    原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。

针对不同block类型的copy、retain、release操作

  • 对block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;
  • 针对NSConcreteGlobalBlock:retain、copy、release操作都无效;
  • 针对NSConcreteStackBlock:retain、release操作无效
    注意的是,NSConcreteStackBlock离开其作用域后,该block内存将被回收,即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在stackBlock离开其作用域失效后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[stackBlock copy]]。
  • NSConcreteMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;
  • 尽量不要对block使用retain操作。因为从上可以看出,retain操作对NSConcreteStackBlock并没有效果,这样会误以为retain生效了,在后续调用block的时候,其实block早就被释放了,从而导致crash