美文网首页
Block截获变量

Block截获变量

作者: Angelia_滢 | 来源:发表于2018-05-09 00:25 被阅读36次

序言

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

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

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

截获变量的值

这一情况主要发生在

.对基本数据类型的引用(局部参数)

来看基本数据常量

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

截获内存地址

.对于静态变量,全局变量,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的偏移量,若有大佬知道确切答案,希望能不吝赐教

写博文真心不易,消耗脑回路比敲代码多,路过的大佬们若有收获打赏一支雪糕吧

相关文章

  • Block(匿名函数)

    一、语法格式 二、截获自动变量值和__block修饰符 block的自动变量截获只针对block中使用的自动变量。...

  • Block

    一、Block本质 二、 BlocK截获变量 三、__block 修饰变量 四、Block内存管理 五、Block...

  • 2022-04-13 block笔记

    什么是block 什么是block调用 block截获变量 __block修饰符用在什么场景,__block变量_...

  • __block

    __ block Block中所使用的被截获自动变量就如“带有自动变量值的匿名函数”所说,仅截获自动变量的值,Bl...

  • iOS知识整理-Block

    Block是将函数及其执行上下文封装起来的对象 变量截获 局部变量截获 是值截获 〜局部静态变量截获 是指针截获 ...

  • iOS block详细知识点

    Block与外界变量 1、截获自动变量(局部变量)值 (1)默认情况 对于 block 外的变量引用,block ...

  • iOS Block面试题(Block变量截获)

    Block变量截获 1、局部变量截获 是值截获。 比如: 这里的输出是6而不是2,原因就是对局部变量num的截获是...

  • Block实现原理

    Block是带有自动变量值的匿名函数; 带有自动变量值在Block中表现为截获自动变量值; 自动变量值截获只能保存...

  • iOS面试之Block大全

    Block Block内容如下: 关于Block 截获变量 __block修饰符 Block的内存管理 Block...

  • iOS面试之Block模块

    Block Block内容如下: 关于Block 截获变量 __block修饰符 Block的内存管理 Block...

网友评论

      本文标题:Block截获变量

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