本文是对 Objective-C Block 的讲解。随着 Swift 日益成熟,Objective-C 地位在日益下滑,但是并不会完全消失,一是很多已有的大应用都有很多 Objective-C 存量代码,如果要使用 C/C++ 的一些跨平台的库,使用 Objective-C 编写 Objective-C++ 做桥接是唯一的选择。多说一点 Swift 可以直接使用 C 的库。

Objective C

什么是 Block

Blocks 是 C 语言的扩充功能,也就是带有自动变量(局部变量)的匿名函数。

匿名函数

匿名函数就是不带有名称的函数,C 语言的标准中函数和函数指针都要用到函数名称,所以不允许存在这样的函数:

int func(int count);
int (*funcptr)(int) = &func;

带有自动变量值

C 语言的函数中可能使用的变量:

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

其中在函数的多次调用之间能够传递值的变量有:

  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

响应按钮点击回调的示例:

int buttonId = 0;

void buttonCallback(int event) {
  printf("buttonId:%d event=%d\n", buttonId, event);
}

如果只有一个按钮,上面代码就没有问题,但是有多个按钮,下面代码就有问题:

void setButtonCallbacks() {
  for (int i = 0; i < BUTTON_MAX; ++i) {
    buttonId = i;
    setButtonCallback(BUTTON_IDOFFSET + i, &buttonCallback);
  }
}

可以通过创建一个类来封装 buttonId 和回调函数:

@interface ButtonCallbackObject : NSObject {
    int buttonId_;
}

@end

@implementation ButtonCallbackObject

- (id)initWithButtonId:(int)buttonId {
    self = [super init];
    if (self) {
        buttonId_ = buttonId;
    }
    return self;
}

- (void)callback:(int)event {
    NSLog(@"buttonId:%d event=%d\n", buttonId_, event);
}

@end
void setButtonCallbacks() {
    for (int i = 0; i < BUTTON_MAX; ++i) {
        ButtonCallbackObject *callbackObj = [[ButtonCallbackObject alloc] initWithButtonId:i];
        setButtonCallbackUsingObject(BUTTON_IDOFFSET + i, callbackObj);
    }
}

创建一个类来解决这个问题就搞复杂了,用 Block 更适合解决这个问题,Block 中会捕获外部局部变量,也就是 “带有自动变量值” 的含义:

void setButtonCallbacks() {
    for (int i = 0; i < BUTTON_MAX; ++i) {
        setButtonCallbackUsingBlock(BUTTON_IDOFFSET + i, ^(int event) {
            printf("buttonId:%d event=%d\n", i, event);
        });
    }
}

Block 语法

Block 表达式语法

完整形式:

^ 返回值类型 参数列表 表达式
^void (int event) {
    printf("buttonId:%d event=%d\n", i, event);
}

省略返回值类型:

^(int event) {
    printf("buttonId:%d event=%d\n", i, event);
}

省略返回值类型和参数列表

^ {
    printf("Blocks\n");
}

Block 类型变量

声明 Block 类型变量:

void (^simpleBlock)(void);

给 Block 类型变量赋值:

simpleBlock = ^{
    NSLog(@"This is a block");
};

声明和赋值放在一起:

void (^simpleBlock)(void) = ^{
    NSLog(@"This is a block");
};

使用 typedef 简化语法

typedef void (^XYZSimpleBlock)(void);

XYZSimpleBlock anotherBlock = ^{
    ...
};

- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
    ...
    callbackBlock();
}

截获自动变量值

“带有自动变量值” 的含义就是 “截获自动变量值”,如下示例 Block 会截获外部的 anInteger 为 Block 自己的自动变量,截获后,再修改外部的自动变量,对已截获的自动变量没有影响。

- (void)testMethod {
    int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
    
    anInteger = 84;
 
    testBlock();
}
Integer is: 42

__block

上面一个示例中截获的自动变量在 Block 内部不能修改其值,要想修改,就需要给截获的自动变量前添加 __block 说明符,说明此自动变量在 Block 的外部和内部共享存储空间,在 Block 外部可以修改:

__block int anInteger = 42;

void (^testBlock)(void) = ^{
    NSLog(@"Integer is: %i", anInteger);
};

anInteger = 84;

testBlock();
Integer is: 84

在 Block 内部也可以修改:

__block int anInteger = 42;

void (^testBlock)(void) = ^{
    NSLog(@"Integer is: %i", anInteger);
    anInteger = 100;
};

testBlock();
NSLog(@"Value of original variable is now: %i", anInteger);
Integer is: 42
Value of original variable is now: 100

截获 Objective-C 对象

截获一个可变数组实例的指针,通过指针修改可变数组的内容,并没有改指针,所以没有问题。

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

但是,如下的赋值就会改变指针的值,就有问题。

id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    array = [[NSMutableArray alloc] init];
}

加上 __block 说明符就没有问题了。

__block id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    array = [[NSMutableArray alloc] init];
}

Block 实现

Block 实质

Block 也是 Objective-C 对象,看下图:

  • Objective-C 对象都有一个 void *isa 说明对象是什么类;
  • invoke 是 C 语言的函数指针,也就 Block 中包裹的代码;
  • descriptor 是用来描述这个 Block,其中 size 表明了 Block 的结构体实例的大小。

Objective-C Block

“截获自动变量值” 意味着执行 Block 语法时,Block 语法表达式所使用的自动变量值被保存到 Block 的结构体实例(即 Block 自身)中,所以不能改动到原有的自动变量值。

添加了 __block 说明符的变量,会变成结构体实例,上图中的 Block 结构体会含有此结构体实例的指针,这样就可以改动到原有的自动变量值。

Block 存储域

如下图的一个程序的内存空间分布,有 text 程序段、data 数据段、stack 栈和 heap 堆,Block 可以放在如下三个存储域:

  • 数据段:Global Block。
  • 堆:Heap Block。
  • 栈:Stack Block。

Program Memory Layout

Global Block 的产生时机:

  • 记述全局变量的地方有 Block 语法。
  • Block 语法的表达式中不使用截获的自动变量。

栈上的 Block 会复制到堆的时机:

  • 调用 Block 的 copy 实例方法。
  • Block 作为函数返回值时。
  • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或 Block 类型成员变量时。
  • 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时。

Block 循环引用

如下代码中 self 是一个 UIViewController 的实例,此 UIViewController 强引用 HVUserAgreementViewController,HVUserAgreementViewController 强引用 confirmBlock,confirmBlock 强引用 self,这样形成了一个循环引用,通过 __weak 修饰符来转换 self 为弱引用,解除掉了循环引用关系。

- (void)openEULAViewController {
    __weak typeof(self) weakSelf = self;
    HVUserAgreementViewController *viewController = [HVUserAgreementViewController new];
    viewController.confirmBlock = ^{
        weakSelf.eulachecker.check = YES;
    };
    [self presentViewController:viewController animated:YES completion:nil];
}

再来看一个情况,如下的 Block 中有多条语句执行,在语句执行的过程中,weakSelf 都有可能变成 nil:

- (void)openEULAViewController {
    __weak typeof(self) weakSelf = self;
    HVUserAgreementViewController *viewController = [HVUserAgreementViewController new];
    viewController.confirmBlock = ^{
        [weakSelf doA];
        [weakSelf doB];
    };
    [self presentViewController:viewController animated:YES completion:nil];
}

进一步修改代码,如下的 Block 中有多条语句执行,在语句执行的过程中,strongSelf 不会变成 nil,因为 __strong 修饰符修饰的变量,再赋值时使引用计数 +1,在 Block 执行结束后,变量被销毁,使引用计数 -1,所以,在语句执行的过程中,strongSelf 不会变成 nil:

- (void)openEULAViewController {
    __weak typeof(self) weakSelf = self;
    HVUserAgreementViewController *viewController = [HVUserAgreementViewController new];
    viewController.confirmBlock = ^{
        __strong typeof(self) strongSelf = weakSelf;
        [strongSelf doA];
        [strongSelf doB];
    };
    [self presentViewController:viewController animated:YES completion:nil];
}