__block 说明符的作用以及其对Clang编译器的影响

Posted Haley_Wong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了__block 说明符的作用以及其对Clang编译器的影响相关的知识,希望对你有一定的参考价值。

__block 不同于 __strong__weak,后面两个是所有权说明符,而__block是存储域说明符,该说明符会影响变量的存储域。

常见的存储域说明符还有:

  • typedef
  • extern
  • static
  • auto
  • register

存储域说明符用于指定将变量值设置到哪个存储域中。

auto表示修饰的变量作为局部变量存储在栈中,这是函数或方法内部默认的声明方式,一般不用添加。auto int count该语句声明index为一个自动局部变量,意味着进入该模块时,自动为其分配存储空间,退出该模块时自动解除分配。

//下面这两句是等价的
int count;
auto int count;

static 表示修饰的变量作为静态变量存储在全局变量区。

static 变量有默认的初始值0,而auto变量没有默认的初始值。除非显式的为auto变量赋值,否则它的值是不确定的。

extern修饰的全局变量,也是存储在全局变量区,虽然都是存储在全局变量区,但是externstatic还是有很大区别的:extern表示修饰的全局变量默认是有外部链接的,作用域是整个工程。在一个文件内定义的全局变量,在另一个文件中,通过extern声明全局变量,就可以使用全局变量;static修饰的全局静态变量,作用域是声明此变量所在的文件。

我们前几篇中有提到过,在block中使用局部变量时,是不能为变量重新赋值的。
因为block内会创建一个新的变量来存储局部变量的值或者对象的指针,修改的其实不是布局变量,因此编译器就干脆报个错误。
但是如果block中存储局部变量的指针,又可能出现变量被释放,访问野指针的情况而导致Crash。

现在介绍一种新的方法:使用__block说明符就可以在block内中修改外部变量的值(即可以为变量赋值)。

那为何添加__block,就可以重新赋值?
添加__block,编译器对源码的转换会发生什么变化呢?

1.源码实战

首先,依然是编写一段测试代码,使用clang -rewrite-objc命令,将源码转换为C语言代码。

转换前的原始代码如下:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) 
    __block int count = 3;
    __block int length = 10;
    void (^blk)(void) = ^ 
        count = 5;
        length = 20;
        NSLog(@"__block 测试 %d", count);
    ;
    count = 10;
    length = 100;
    blk();
    return 0;

经Clang 转换后的Block相关C语言代码如下:

// 被__block修饰的变量会被转换为如下结构体
struct __Block_byref_count_0 
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
;

struct __Block_byref_length_1 
  void *__isa;
__Block_byref_length_1 *__forwarding;
 int __flags;
 int __size;
 int length;
;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
  __Block_byref_count_0 *count = __cself->count; // bound by ref
  __Block_byref_length_1 *length = __cself->length; // bound by ref

        (count->__forwarding->count) = 5;
        (length->__forwarding->length) = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rp_rp_9nrd50kq_0n87d_r7fn1c0000gn_T_main_1c6300_mi_0, (count->__forwarding->count));
    
    
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) _Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->length, (void*)src->length, 8/*BLOCK_FIELD_IS_BYREF*/);

static void __main_block_dispose_0(struct __main_block_impl_0*src) _Block_object_dispose((void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->length, 8/*BLOCK_FIELD_IS_BYREF*/);

static struct __main_block_desc_0 
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
 __main_block_desc_0_DATA =  0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0;

int main(int argc, const char * argv[]) 
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = (void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 3;
    __attribute__((__blocks__(byref))) __Block_byref_length_1 length = (void*)0,(__Block_byref_length_1 *)&length, 0, sizeof(__Block_byref_length_1), 10;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, (__Block_byref_length_1 *)&length, 570425344));
    (count.__forwarding->count) = 10;
    (length.__forwarding->length) = 100;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;

2.分析

首先,Clang会将__block修饰的变量,转换出一个结构体类型。
结构体类型命名是__Block_byref_ + 变量名 + _ + index(变量出现的顺序)。

所以这里使用的两个变量类型分别是:__Block_byref_count_0__Block_byref_length_1

而该__block结构体类型的结构是:

  • 第一个参数__isa指针,
  • 第二个参数__forwarding指针,指向自己的内存地址。
  • 第三个参数__flags,是标志性参数,暂时没用到所以默认为0。
  • 第四个参数__size,是该结构体所占用的大小。
  • 第五个参数就是与外部类型和名称一致的实际变量。

__main_block_impl_0基本上跟上一节一样,依然是有implDesc两个实例变量,以及block内部使用到的两个新的__block变量类型的变量。

注意
__main_block_impl_0中存储的并不是__Block_byref_count_0__Block_byref_length_1两个变量,而是这两个变量的指针地址,这样做是为了能够在多个block中访问到同一个__block变量。

需要特别注意的是__main_block_func_0函数的实现。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
  __Block_byref_count_0 *count = __cself->count; // bound by ref
  __Block_byref_length_1 *length = __cself->length; // bound by ref

        (count->__forwarding->count) = 5;
        (length->__forwarding->length) = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rp_rp_9nrd50kq_0n87d_r7fn1c0000gn_T_main_1c6300_mi_0, (count->__forwarding->count));
    

这里首先依然是通过函数传入的形参__cself,取到内部的实例变量count 和length。

count 和length分别是__Block_byref_count_0类型和__Block_byref_length_1

其实这里,可以直接使用count->count = 5以及length->length = 20就完事了。

  • 那为什么要添加__forwarding这个实例变量?
  • 为什么要(count->__forwarding->count) = 5这样赋值呢?

添加__forwarding是为了存储变量的内存地址,在对block进行copy之前,__forwarding中存的是自己的内存地址,所以访问count->__forwarding就等于访问自身;但是对block进行copy,从栈区拷贝到堆区后,__Block_byref_count_0这样的变量也会从栈区拷贝到堆区,所以就会有一个生成一个新的变量,这时候原来的变量里的count->__forwarding存储的就是堆区中的变量的内存地址。

所以,(count->__forwarding->count) = 5这样赋值,可以保证不管变量是否从栈区拷贝到了堆区,都能正确的访问实际的变量。

__main_block_copy_0__main_block_dispose_0这俩函数,就是为了系统在合适的时机,将block从栈区拷贝到堆区时,对变量进行赋值和销毁。

最后来看看最关键的main(),去掉一些多余的转换如下:

int main(int argc, const char * argv[]) 
     __Block_byref_count_0 count = (void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 3;
     __Block_byref_length_1 length = (void*)0,(__Block_byref_length_1 *)&length, 0, sizeof(__Block_byref_length_1), 10;
    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &count, &length, 570425344));
    (count.__forwarding->count) = 10;
    (length.__forwarding->length) = 100;
    (*blk->impl.FuncPtr)(blk);
    return 0;

这里首先创建了两个新结构体类型的变量__Block_byref_count_0 count__Block_byref_length_1 length
然后,再构造一个__main_block_impl_0实例。

另外,还有一个需要注意的是,__main_block_impl_0构造函数的最后一个参数Flags不再是默认的0了。

如果在block外,修改__block变量,则也使用(count.__forwarding->count)来赋值。可以保证不管是block内,还是block外访问的都是同一个变量。

注意 和 总结
这里只有block中使用到了__block变量,Flags才不是默认值0。
count.__forwarding->count这样访问,可以保证所访问变量的一致性(不管是否经过栈到堆的拷贝)。

以上是关于__block 说明符的作用以及其对Clang编译器的影响的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发面试只需知道这些,技术基本通关!(block篇)

使用 clang-llvm 编译器在 CUDA 中添加对类似于 __shared__ 的内存类型的支持

关于block以及__bridge的一些笔记

强制函数在Clang / LLVM中内联

Clang 是如何编译Block的?

Block的原理和使用