Block基础


Block是函数及其执行上下文封装起来的对象。

Block结构

使用Block的时候,编译器对Block语法进行了怎样的转换?

int main() {
    int count = 10;
    void (^ blk)() = ^(){
        NSLog(@"In Block:%d", count);
    };
    blk();
}

如上所示的最简单的Block使用代码,经clang转换后,可得到以下几个部分(有代码删减和注释添加):

static void __main_block_func_0(
    struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0, 
    count);
}

这是一个函数的实现,对应Block中{}内的内容,这些内容被当做了C语言函数来处理,函数参数中的__cself相当于Objective-C中的self。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; //描述Block大小、版本等信息
  int count;
  //构造函数函数
  __main_block_impl_0(void *fp,
          struct __main_block_desc_0 *desc,
          int _count,
          int flags=0) : count(_count) {
    impl.isa = &_NSConcreteStackBlock; //在函数栈上声明,则为_NSConcreteStackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0即为main()函数栈上的Block结构体,其中的__block_impl结构体声明如下:

struct __block_impl {
  void *isa;//指明对象的Class
  int Flags;
  int Reserved;
  void *FuncPtr;
};

__block_impl结构体,即为Block的结构体,可理解为Block的类结构。
再看下main()函数翻译的内容:

int main() {
    int count = 10;
    void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

去除掉复杂的类型转化,可简写为:

int main() {
    int count = 10;
    sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,         //函数指针
                                                           &__main_block_desc_0_DATA, count)); //Block大小、版本等信息
    
    (*blk->FuncPtr)(blk);   //调用FuncPtr指向的函数,并将blk自己作为参数传入
}

由此,可以看出,Block也是Objective-C中的对象。
Block有三种类(即__block_impl的isa指针指向的值,isa说明参考《Objective-C isa 指针 与 runtime 机制》),根据Block对象创建时所处数据区不同而进行区别:

_NSConcreteStackBlock:在栈上创建的Block对象

_NSConcreteMallocBlock:在堆上创建的Block对象

_NSConcreteGlobalBlock:全局数据区的Block对象

如何截获自动变量

上部分介绍了Block的结构,和作为匿名函数的调用机制,那自动变量截获是发生在什么时候呢?答案是创建block结构体的时候,既不是声明的时候,也不是调用的时候。

观察上节代码中__main_block_impl_0结构体(main栈上Block的结构体)的构造函数可以看到,栈上的变量count以参数的形式传入到了这个构造函数中,此处即为变量的自动截获。

因此可以这样理解:__block_impl结构体已经可以代表Block类了,但在栈上又声明了__main_block_impl_0结构体,对__block_impl进行封装后才来表示栈上的Block类,就是为了获取Block中使用到的栈上声明的变量(栈上没在Block中使用的变量不会被捕获),变量被保存在Block的结构体实例中。

所以在blk()执行之前,栈上简单数据类型的count无论发生什么变化,都不会影响到Block以参数形式传入而捕获的值。但这个变量是指向对象的指针时,是可以修改这个对象的属性的,只是不能为变量重新赋值。

Block的存储域

上文已提到,根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。

void (^blk)(void) = ^{
    NSLog(@"Global Block");
};

int main() {
    blk();
    NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
}

像上面代码块中的全局blk自然是存储在全局数据区,但注意在函数栈上创建的blk,如果没有截获自动变量,Block的结构实例还是会被设置在程序的全局数据区,而非栈上:

int main() {
    void (^blk)(void) = ^{//没有截获自动变量的Block
        NSLog(@"Stack Block");
    };
    blk();
    NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
    
    int i = 1;
    void (^captureBlk)(void) = ^{//截获自动变量i的Block
        NSLog(@"Capture:%d", i);
    };
    captureBlk();
    NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__
}

可以看到没有截获自动变量的Block打印的类是NSGlobalBlock,表示存储在全局数据区。
但为什么捕获自动变量的Block打印的类却是设置在堆上的NSMallocBlock,而非栈上的NSStackBlock?这个问题稍后解释。

Block复制

配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。
复制到堆上的Block,将_NSConcreteMallocBlock类对象写入Block结构体实例的成员变量isa:

impl.isa = &_NSConcreteMallocBlock;

在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:

调用Block的copy方法
将Block作为函数返回值时
将Block赋值给__strong修改的变量时
向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时

其它时候向方法的参数中传递Block时,需要手动调用copy方法复制Block。
上一节的栈上截获了自动变量i的Block之所以在栈上创建,却是NSMallocBlock类,就是因为这个Block对象赋值给了_strong修饰的变量captureBlk(_strong是ARC下对象的默认修饰符)。
因为上面四条规则,在ARC下其实很少见到_NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是NSStackBlock:

int count = 0;
    blk_t blk = ^(){
        NSLog(@"In Stack:%d", count);
    };
    
    NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
    NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
    NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
    NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__

关于ARC下和MRC下Block自动copy的区别,查看《Block 小测验》里几道题目就能区分了。
另外,原书存在ARC和MRC混合讲解、区分不明的情况,比如书中几个使用到栈上对象导致Crash的例子是MRC条件下才会发生的,但书中没做特殊说明。

使用__block发生了什么

Block捕获的自动变量添加__block说明符,就可在Block内读和写该变量,也可以在原来的栈上读写该变量。
自动变量的截获保证了栈上的自动变量被销毁后,Block内仍可使用该变量。
__block保证了栈上和Block内(通常在堆上)可以访问和修改“同一个变量”,__block是如何实现这一功能的?
__block发挥作用的原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。
验证过程:
现在对刚才的代码段,加上__block说明符,并在block内外读写变量count。

int main() {
    __block int count = 10;
    void (^ blk)() = ^(){
        count = 20;
        NSLog(@"In Block:%d", count);//打印:In Block:20
    };
    count ++;
    NSLog(@"Out Block:%d", count);//打印:Out Block:11
    
    blk();
}

将上面的代码段clang,发现Block的结构体__main_block_impl_0结构如下所示:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

最大的变化就是count变量不再是int类型了,count变成了一个指向__Block_byref_count_0结构体的指针,__Block_byref_count_0结构如下:

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

它保存了int count变量,还有一个指向__Block_byref_count_0实例的指针__forwarding,通过下面两段代码__forwarding指针的用法可以知道,该指针其实指向的是对象自身:

//Block的执行函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref

        (count->__forwarding->count) = 20;//对应count = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0, 
        (count->__forwarding->count));
    }
//main函数
int main() {
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,
    (__Block_byref_count_0 *)&count, 
    0, 
    sizeof(__Block_byref_count_0), 
    10};
    
    void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
    &__main_block_desc_0_DATA, 
    (__Block_byref_count_0 *)&count, 
    570425344));
    
    (count.__forwarding->count) ++;//对应count ++;
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1, 
    (count.__forwarding->count));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

为什么要通过__forwarding指针完成对count变量的读写修改?
为了保证无论是在栈上还是在堆上,都能通过__forwarding指针找到在堆上创建的count这个__Block_byref_count_0结构体,以完成对count->count(第一个count是__Block_byref_count_0对象,第二个count是int类型变量)的访问和修改。保证所有的block都修改的是堆上的值,避免不一致。

示意图如下:

注意block或者byref结构拷贝前后,__forwarding指针的指向。思考:为什么要这么做?为了数据一致性,都修改的是堆上的。

总结:
没访问外部变量,或访问全局变量、全局静态变量(无捕获),类型为__NSGlobalBlock__
访问外部非全局变量和全局静态变量(有捕获):
被强指针引用或被拷贝,类型为__NSMallocBlock__
被弱指针引用或未被指针引用,类型为__NSStackBlock__
block内对全局变量和全局静态变量是直接访问的,对局部静态变量采用指针访问。

block的捕获:

类型 捕获
局部基本数据 捕获其值
局部对象类型 连同所有权修饰符一起捕获(编译成C++代码时,block定义里的构造函数参数上会有所有权修饰符,例如__strong,__weak等)
局部静态变量 以指针形式捕获
全局变量 不捕获
全局静态变量 不捕获

__block修饰的变量变成了对象。
栈上block变量对象的__forwarding指针指向自己

Block的copy

Block类型 copy结果
__NSConcreteStackBlock
__NSConcreteGlobalBlock 数据区 什么也不做
__NSConcreteMallocBlock 增加引用计数

对栈上的block进行copy,会在堆上创建block及其__block变量。栈上__block变量的__forwarding指针指向堆上的__block变量。堆上__block变量的__forwarding指针指向自身。不论在任何内存位置,都可以顺利访问同一个__block成员变量。

参考

https://www.jianshu.com/p/d28a5633b963
https://www.jianshu.com/p/4e79e9a0dd82
https://www.jianshu.com/p/5cbc7d8154cd

Block技巧与底层解析 https://www.jianshu.com/p/51d04b7639f1

iOS 关于 __block 底层实现机制的疑问?
https://www.zhihu.com/question/39980914

猜你喜欢

转载自blog.csdn.net/Mamong/article/details/124680560
今日推荐