iOS - Block 原理探究(2)

2019-06-16

之前有篇文章是探究 block 的原理,了解了 block 的 3 种类型

最近又看到 __block 修饰对象的 __forwording 指针在不同类型的 block 对象中表现不同,因此写了一些代码进行测试,并发现我之前对于 block 3 中类型的理解是有偏颇的,因此没有很好的理解 __forwording 指针

1. Block 类型

我们先看以下代码会输出什么

- (void)viewDidLoad {
    [super viewDidLoad];

    void(^ block_1)(void) = ^(void) {
        
    };
    NSLog(@"block_1: %@", block_1);
    
    int a = 0;
    void(^ block_2)(void) = ^(void) {
        a;
    };
    NSLog(@"block_2: %@", block_2);
}

我一开始以为应该会输出,因为这两个 block 不都是局部变量吗,那么应该放在栈区啊

block_1: <__NSStackBlock__: 0x...> 	// 错误
block_2: <__NSStackBlock__: 0x....>  // 错误

那么我们看看正确答案

block_1: <__NSGlobalBlock__: 0x...> 	// 正确
block_2: <__NSMallocBlock__: 0x....>  // 正确

那么我们分析一下为什么会这样

我们先看看 block_2 的原因,因为了解了 block_2 有助于了解 block_1

为什么 block_2 是一个堆区的 block 呢,明明没有进行栈区 block 的 copy 操作啊,那我们再看看以下代码就会有种恍然大悟的感觉。就如注释所说,其实我们那段代码并非是初始化一个 栈区 block,而是将一个 栈区block 赋值给 block_2 这个变量 。而在 ARC 中通过强引用一个block,会将其 copy 到堆区。因此 block_2 是一个 堆区 block

int a = 0;

// 这个 block_1 才是一个 栈区的 block
void *block_1 = (__bridge void *)^(void) {
    a;
}();

// 而这个表示的是将一个 栈区的block 赋值给 block_2 这个变量
void(^block_2)(void) = ^(void) {
	a;
};
block();

接下来我们看看 block_1,首先我要纠正我一个错误的思想,判断一个 block 是 栈区 的还是 全局区 的一句不应该是它是否是局部变量,而是 block 中是否访问了 auto 变量,因此上面 block_1 是 copy 了一个 全局区的 block,那么还是一个全局区的 block。因此 block_1 是一个全局区的 block。

那么我们总结 3 种类型的 block

  __NSStackBlock__ __NSGlobalBlock__ __NSMallocBlock__
存放区域 栈区 全局区(静态区) 堆区
类型说明 block 访问了 auto 变量 block 没有访问 auto 变量 copy 或者 NSStackBlock 类型后生成的 block 为 NSMallocBlock类型
copy 操作会发生什么 会在堆区生成一个 NSMallocBlock 类型block 还是一个 NSGlobalBlock 会对当前 block 引用计数 +1

2. __forwording 指针

对 block 的 3 种类型了解后,就可以写测试代码看我们 __forwording 指针的原理

__block int a = 1;
printf("原始 __forwording 地址: %p\n", &a);


^(void) {
	printf("栈区 __forwording 地址: %p\n", &a);
}();

这时输出以下,是一样的,说明 栈区 block 的 __block 对象的 __forwording 指针就是指向自己

原始 __forwording 地址: 0x7ffeea4c4928
栈区 __forwording 地址: 0x7ffeea4c4928

我们再看

__block int a = 1;
printf("原始 __forwording 地址: %p\n", &a);

^(void) {
    printf("原始栈区 __forwording 地址: %p\n", &a);
}();

// copy block
void(^ block)(void) = ^(void) {
    printf("堆区 __forwording 地址: %p\n", &a);
};

block();

^(void) {
    printf("copy后栈区 __forwording 地址: %p\n", &a);
}();

这时输出以下,说明 __block 对象 a 在 copy 到堆上之后, 它的 __forwording 指针都指向堆上的 a 对象

原始 __forwording 地址: 0x7ffeea51e928
原始栈区 __forwording 地址: 0x7ffeea51e928
堆区 __forwording 地址: 0x6000002a2298
copy后栈区 __forwording 地址: 0x6000002a2298