Block自动截获变量

《Block前言》中讲到,不论何种类型的Block都自带截获变量这一技能,而针对不同的变量类型和不同的情况,自动截获分为以下情况


1.截获变量的值
2.截获对象,将对象指针传递进去
3.将变量拷贝到堆区域,并持有变量
4.截获变量内存地址

现针对以上内容进行详细分析。

截获变量的值

这一情况主要发生在
1.对基本数据类型的引用(局部参数)
其实说白了,对于所有类型,Block自动截获的皆为在Block截获之前的变量的瞬间值,唯一不同的是如果是Object类型,Block会多一步copy操作。先来看基本数据常量

int a = 0;
void (^lockBlock)(void) = ^{
        NSLog(@"a = %d",a);
};
++a;
lockBlock();
NSLog(@"%@", lockBlock);

以上代码最后输出

YAObjectTest[7397:1142111] a = 0
 YAObjectTest[7397:1142111] <__NSMallocBlock__: 0x604000443e10>

发现a的值在执行block之前做了修改,执行block后获取到的还是a的原来值。
查看编译后的cpp文件

void (*lockBlock)(void) = ((void (*)())&__BlockObject__testBlockAutomaticInterceptVar_block_impl_0((void *)__BlockObject__testBlockAutomaticInterceptVar_block_func_0, &__BlockObject__testBlockAutomaticInterceptVar_block_desc_0_DATA, a));

可以看到传入lockBlock结构体中的仅有a的值,再看_block_impl_0中

struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0
{
  struct __block_impl impl;
  struct __BlockObject__testBlockAutomaticInterceptVar_block_desc_0* Desc;
  int a;
  __BlockObject__testBlockAutomaticInterceptVar_block_impl_0(void *fp, struct __BlockObject__testBlockAutomaticInterceptVar_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

该部分的第五行int a;可以明确的看到a 是值的形式存在。

为何会是引入a的值而不是a的内存地址呢?主要原因是int a 和LockBlock的存储区域不同,因int a = 0的声明是在函数内,所以是在栈区,而lockBlock在引用了局部变量后转换为MallocBlock存放在堆区

在上述Block的实现函数__BlockObject__testBlockAutomaticInterceptVar_block_func_0中,我们可以看到如下部分

int a = __cself->a; // bound by copy

系统自动给我们加上了注释,bound by copy,变量int a ,是用 __cself-> 来访问的,Block仅仅捕获了 a 的值,并没有捕获a的内存地址。
所以在testBlockAutomaticInterceptVar`这个函数中后来即使我们重写int a 的值,依旧无法去改变Block外面变量a的值



也正是基于以上原因,我们无法在Block内部更改自动截获的变量,更改截获的自动变量编译器会报以下错误

Variable is not assignable (missing __block type specifier)

变量无法在Block中改变外部变量的值,所以编译过程中就报编译错误



截获对象,将对象指针传递进去,并持有变量

相比较于基本数据常量而言,Block截获Object上,会有区分,Block截获的是对象,传入的是对象的指针,但是会多传入一部分内容,而且会多一步copy操作

 NSString *testString = @"It is just a joke";
 void (^lockBlock)(void) = ^{
        [testString stringByAppendingString:@"Yeah, I'm sure"];
  };
   lockBlock();
  NSLog(@"%@",testString);
  NSLog(@"%@", lockBlock);

用以上OC代码运行会发现testString的内存地址是一样的<__NSArrayM 0x604000240090>,同样不能在Block内部进行初始化操作(因为重新初始化Block内部的引用对象内存地址会发生变化这是不允许的)。

查看clang后的cpp文件,我们发现lockBlock声明赋值的部分编译后的代码如下

void (*lockBlock)(void) = ((void (*)())&__BlockObject__testBlockAutomaticInterceptVar_block_impl_0((void *)__BlockObject__testBlockAutomaticInterceptVar_block_func_0, &__BlockObject__testBlockAutomaticInterceptVar_block_desc_0_DATA, testString, 570425344));

相比较于基本数据常量而言,传递参数多了后面的570425344(这一部分后面探讨)。其它和基本数据类型一样,直接以NSString *testString;出现在__BlockObject__testBlockAutomaticInterceptVar_block_impl_0结构体中,在__BlockObject__testBlockAutomaticInterceptVar_block_func_0结构体中以
NSString *testString = __cself->testString; // bound by copy——cself-> 形式调用。

引用对象不同的是会多出来以下函数

static void __BlockObject__testBlockAutomaticInterceptVar_block_copy_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*dst, struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_assign((void*)&dst->testString, (void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __BlockObject__testBlockAutomaticInterceptVar_block_dispose_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_dispose((void*)src->testString, 3/*BLOCK_FIELD_IS_OBJECT*/);}

在编译文件中看到引用对象时有_block_copy 和 _block_dispose函数。这两个函数的作用相当于内存管理MRC中的copy 和 release操作,调用_block_copy将引用对象进行copy操作,调用_block_dispose相当于对testString 进行release操作。
copy具体的执行操作是申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数,dispose函数正好相反。
copy和dsipose函数中最后一个参数代表截获的参数类型,3 代表是Block,编译后的代码中注释了BLOCK_FIELD_IS_OBJECT,其它形式如下
.BLOCK_FIELD_IS_BLOCK;
.BLOCK_FIELD_IS_WEAK;
.BLOCK_BYREF_CALLER
.BLOCK_FIELD_IS_BYREF

与截获基本数据类型相同,截获对象传递的是指针,所以在Block内不能再对对象进行初始化,但其本身自带的方法可以调用,MallocBlock会持有引用的变量。

#变量拷贝到堆区域,并持有变量
.__block 修饰符修饰

#block
对于对象,Block引用内部可以进行操作不能初始化,但对于基本数据类型如何进行更改呢,这个时候会用到`
block`修饰符。该修饰符的主要作用是将基本数据常量写入结构体转变为对象,copy到堆上,持有变量。来看下代码和转换后的代码

 __block int a = 0;
 void (^lockBlock)(void) = ^{
      a = 2;
 };

编译后的代码

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
  void (*lockBlock)(void) = ((void (*)())&__BlockObject__testBlockAutomaticInterceptVar_block_impl_0((void *)__BlockObject__testBlockAutomaticInterceptVar_block_func_0, &__BlockObject__testBlockAutomaticInterceptVar_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

可以看到int a 被转换为_blocks__(byref)类型,在Block使用时传入的(__Block_byref_a_0 *)&a,而具体的a被转换后的结构体,

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

和对象的结构体一样包含isa指针,并且还有一个forwarding指针,flags、size、和一个int a 。此时,发现int a作为结构体成员,而forwarding指针是指向其本身,这就保证了被拷贝到堆区之后依然能够找到该变量。
将参数转变成对象之后,其也会增加

static void __BlockObject__testBlockAutomaticInterceptVar_block_copy_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*dst, struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __BlockObject__testBlockAutomaticInterceptVar_block_dispose_0(struct __BlockObject__testBlockAutomaticInterceptVar_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

copy和dispose函数的最后一个参数变为8,意味截获的变量是block转换来的。
具体的关于copy和dispose 可以祥见[霜神博客《深入研究Block捕获外部变量和
block实现原理》第二部分Block的copy和dispose](https://www.jianshu.com/p/ee9756f3d5f6)

#截获内存地址
.对于静态变量,全局变量,Block截获的是内存地址,在Block内部可以直接修改值。
主要因为静态变量和全局变量的存储区域并不会发生改变,所以在Block截获时引用的是其内存地址,修改后仍旧是存储在静态区

static int count = 100;
 typedef int (^blockStatic)(void);
 blockStatic blk = ^(){
    count = 1000;
   return count;
  };

转换后的函数实现如下

static int __BlockObject__testBlockKinds_block_func_0(struct __BlockObject__testBlockKinds_block_impl_0 *__cself) {
        count = 1000;
        return count;
  }

在Block内部直接可以修改count的值,对count的引用直接获取的内存地址,且在__block _impl 结构体中并没有将count值引用或copy。

#结尾补充:”570425344”代表啥?
细心的大佬们肯定发现了在Block语法转换时候,若引用的是对象,则后面必跟一个数字570425344 ,且不管是不同项目、不同类、不同Block,这个数值是固定的。为了这个问题也困惑了好久,开始以为这就是一个判断是否是对象的枚举类型。最后特不好意思的咨询霜大神,醍醐灌顶。可能和霜神之间隔了570425344光年的距离,这距离差在解决问题的思路和办法上,我是一直在编译后的cpp文件中查看,发现并没有解释,只能通过尝试来得出一个猜想。霜神是直接将这串数次Google ,而Google 告诉我们了答案(虽然这答案未必准备,但比我的想法好多了)。

myBlock->impl.isa = &_NSConcreteStackBlock;
myBlock->impl.Flags = 570425344;

570425344为Flags的偏移量,这个偏移量是固定的。大家可以自己代码运行下查看GlobalBlock的Flags为10位数正数,StackBlock和MallocBlock的Flags为10位数负数,
这里暂时将”570425344"理解为Flags的偏移量,若有大佬知道确切答案,希望能不吝赐教