Block的本质

1.Block的实现

我们在命令行下输入
clang -rewrite-objc 源代码文件名
就可以把含有block语法的源代码变换为C++的源代码。

int main(int argc, char * argv[]) {
    
    void (^blk)(void) = ^{
     printf("block");   
    };
    
    blk();
    
    return 0;
}

上面是最简单的block
我们把它转化为C++的源代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

printf("block");  
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, char * argv[]) {

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
  • 1.实现block

void (blk)(void) = ((void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

简化一下:

struct _main_block_impl_0 tmp = _main_block_impl_0(_main_block_func_0, _main_block_desc_0_DATA);
struct _main_block_impl_0 *blk = &tmp;

这一段源码将_main_block_impl_0结构体实例的指针赋值给_main_block_impl_0结构体指针类型的变量。
看一下_main_block_impl_0结构体实例:

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

_main_block_impl_0结构体的构造函数有两个参数,第一个参数需要传入一个函数指针,第二个参数是作为静态全局变量初始化的__main_block_desc_0结构体实例指针,第三个参数有值flags = 0。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

printf("block");
}

这一部分就是相当于block块中的^{};也即匿名函数作为简单的C语言函数来处理。

  • 总结起来实现block就是声明一个结构体,利用结构体的构造函数去初始化结构体中的成员变量,带入参数从初始化之后的成员变量是这样的:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = _main_block_impl_func;
Desc = &_main_block_desc_0_DATA;

初始化里面最有价值的就是初始化impl.FuncPtr。

  • 2 .调用block。
    调用block的源代码是:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

化简一下就是:

(*blk->impl.FuncPtr)(blk);

这就是用结构体指针来调用函数,并且将blk作为参数传入。上面的源码就等价于:

_main_block_func_0(blk);

由此我们也可以看出block是作为参数来传递的。

2.截获自动变量值

变量分为四种,即局部变量,全局变量,静态变量,全局静态变量。这里的局部变量即为自动变量。定义一个静态变量系统会自动初始化,但是定义有一个自动变量系统不会自动帮你初始化,

截获的意思即保存该自动变量的瞬时值,并且在block内不能对该自动变量进行修改。

看下面的代码:

int a = 1;
static int b = 2;

int main(int argc, const char * argv[]) {

    int c = 3;
    static int d = 4;
    NSMutableString *str = [[NSMutableString alloc]initWithString:@"hello"];
    void (^blk)(void) = ^{
        a++;
        b++;
        d++;
        [str appendString:@"world"];
        NSLog(@"1----------- a = %d,b = %d,c = %d,d = %d,str = %@",a,b,c,d,str);
    };
    
    a++;
    b++;
    c++;
    d++;
str = [[NSMutableString alloc]initWithString:@"haha"];
    NSLog(@"2----------- a = %d,b = %d,c = %d,d = %d,str = %@",a,b,c,d,str);
    blk();
    
    return 0;
}

执行结果:

2----------- a = 2,b = 3,c = 4,d = 5,str = haha
1----------- a = 3,b = 4,c = 3,d = 6,str = helloworld

如果在block中对c进行c++操作会报错。
把它转化为c++的源码:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

int a = 1;
static int b = 2;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *d;
  NSMutableString *str;
  int c;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy

        a++;
        b++;
        (*d)++;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_2,a,b,c,(*d),str);
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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[]) {
    int c = 3;
    static int d = 4;
    NSMutableString *str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_0);
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));

    a++;
    b++;
    c++;
    d++;
    str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_4,a,b,c,d,str);
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

我们还是从block的实现中开始看,实现block的源码是:

void (blk)(void) = ((void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));
化简一下,即:
struct _main_block_impl_0 tmp = _main_block_impl_0(_main_block_func_0, _main_block_desc_0_DATA, &d, str, c, 570425344);
struct _main_block_impl_0 *blk = &tmp;

我们看到_main_block_impl_0结构体截获了外部的三个变量,分别是&d,str,c,这里我们就可以知道,结构体截取的是静态局部变量d的地址,指针变量str,还截获了自动变量c的值。
然后看到__main_block_impl_0中多了三个成员变量,即

 int *d;
 NSMutableString*str;
 int c;

然后在_main_block_impl_0这个结构体的构造函数中用截获到的&d,str,c这三个值来分别初始化这三个成员变量。这样,静态局部变量d的地址,指针变量str和实值c就被_main_block_impl_0结构体的成员变量所保存。这样block的实现就全部完成了。

然后看到block的调用,调用block的源码是:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
简化一下:
(*blk->impl.FuncPtr)(blk);
再通俗一点就是:
_main_block_func_0(blk);
我们看一下_main_block_func_0中的实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy

        a++;
        b++;
        (*d)++;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_2,a,b,c,(*d),str);
    }

刚刚我们在block的实现中知道,_main_block_impl_0结构体截取了局部变量,并且赋值给自己的成员变量保存下来。现在_main_block_func_0函数中的三个临时变量指针d,指针变量str,常量c的值就是由_main_block_impl_0结构体的成员变量的值传递过来的。

  • 那么现在问题来了,为什么自动变量在block内不能改变呢?我们梳理一下自动变量c的传递过程:首先是_main_block_impl_0结构体截获了自动变量c的值,然后把这个值赋给了自己的成员变量来保存,在调用block的时候又把这个成员变量的值传给了_main_block_func_0中的临时变量c,问题就在于,这每一步传递都是值传递,所以即使block内部临时变量c的值改变了,真正的自动变量c的值也不会因此改变。既然如此,我猜测苹果干脆就不允许这种无意义的操作存在,因此在block内改变自动变量的值就会报错。
  • 那为什么静态局部变量的值在block内能改变呢?我们还是看一下静态局部变量d的传递过程。首先是_main_block_func_0结构体截获了静态局部变量d的地址,然后把它赋给了结构体的成员变量来保存,在调用block的时候又把这个成员变量的值即静态局部变量d的地址传给了临时指针变量,这样,_main_block_func_0中的临时指针变量c中存放的值就是存放静态局部变量的地址,然后(*d)++就是把这个地址中的值加1,这样静态局部变量的值也就改变了。
  • 对于NSString类型的指针变量str,str变量内存放的是NSString类的对象的存放 地址,由于自动变量的值不能修改,所以这个str指针变量指向的地址不能改变,但是!!!这个地址内存放的对象可以改变,所以我们就能看到_main_block_func_0函数中的执行代码:
    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"),
    这个代码改变了str所指向的地址中的对象,但是没有改变str所指向的地址,这样是可以的。
  • 对于全局变量。它们在一开始的时候根本就没有被_main_block_impl_0结构体所截获,所以也就不存在能不能被修改了。

3.__block修饰符

因为不能改写被截获的自动变量的值,所以当编译器在变异的过程中检查出给被截获的自动变量的赋值操作时变产生编译错误,不过这样一来就无法在block中保存值了,很不方便。因此就产生了__block修饰符。

  • __block是一种存储域类的说明符,类似的还有typedef,extern,static,auto,register。
    __block说明符类似于static,auto,register说明符,它们用于指定将变量值存储到哪个存储域中。例如,auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。
    下面是使用__block修饰符的简单的代码:
int main(int argc, char * argv[]) {
        
        __block int val = 10;
        
        void (^blk)(void) = ^{
            
            val = 1;
        };
    
    return 0;
}

转化为c++的源码:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref


            (val->__forwarding->val) = 1;
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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, char * argv[]) {

        __Block_byref_val_0 val ={
                                             (void*)0,
                                             (__Block_byref_val_0 *)&val, 
                                             0, 
                                             sizeof(__Block_byref_val_0), 
                                             10};

        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    return 0;
}

可以看到增加了__block修饰符后代码量增加了很多。
还是从main函数开始看。可以看到带有__block修饰符的自动变量val被转化成了__Block_byref_val_0结构体类型的实例变量。看一下__Block_byref_val_0结构体:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

其中的_forwarding是一个指向_Block_byref_val_0结构体实例的指针。最后一个成员变量val相当于原自动变量,这意味着该结构体持有相当于原自动变量的成员变量。然后利用_main_block_impl_0的构造函数来初始化自己的成员变量,尤其注意新增的成员变量:_Block_byref_val_0结构体指针。
下面这段给__block变量复制的代码又如何呢?
^{val = 1;}
该源代码转化如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  __Block_byref_val_0 *val = __cself->val; // bound by ref

            (val->__forwarding->val) = 1;
        }

该源代码首先将_main_block_impl_0结构体的成员变量即_Block_byref_impl_0结构体指针传递进来。_Block_byref_val_0结构体实例的成员变量_forwarding持有指向该实例自身的指针。通过成员变量访问成员变量val(成员变量val是该实例自身持有的变量,它相当于原自动变量)。

  • 总结起来,整个数据传递的过程就是_main_block_impl_0结构体截获_Block_byref_val_0结构体实例的指针,然后把它传递给_main_block_impl_0结构体的成员变量保存起来。当要调用block的时候就把_main_block_impl_0结构体的成员变量保存的值传递给_main_block_func_0函数的临时变量,这个临时变量是_Block_byref_val_0结构体指针,然后通过这个结构体指针来调用_Block_byref_val_0结构体的成员变量_forwarding,它是一个指向自身的指针,再通过_forwarding去调用结构体的成员变量val,然后修改它的值。
通过增加__block修饰符是怎么完成修改自动变量的值的呢?
  • 首先应该注意到_main_block_impl_0结构体截获的是_Block_byref_val_0的实例变量val的地址,因此_main_block_impl_0的结构体中新增的成员变量即为指向该实例变量val的指针,然后通过指针去改变结构体中成员变量的值。由于传递的是地址,因此当外部调用时,自动变量val的值已经被成功修改。

4.Block存储域和__block变量存储域

Block存储域

Block也是oc对象,把Block当做oc的对象来看时,Block所属的类有三种,即_NSConcreteStackBlock,_NSConvreteGlobalBlock,_NSConcreteMallocBlock.

  • 首先,_NSConcreteStackBlock中有个stack,就是栈,即该类的对象Block设置在栈上。
  • _NSConvreteGlobalBlock中有global,即即与全局变量一样分配在数据区域中。
  • _NSConcreteMallocBlock类对象设置在由malloc函数分配的内存块即堆中。
    我们在前面经常看到这样的初始化:impl.isa = &_NSConcreteStackBlock。这就说明该Block属于_NSConcreteStackBlock类,分配在栈区。
1. _NSConvreteGlobalBlock

在以下两种情况下的任意一种情况都是将Block分配在数据区,即属于_NSConvreteGlobalBlock类。

  • Block用在定义全局变量的地方。
  • Block并没有截取自动变量的值。
    第一种情况的示例代码如下:
void (^blk)(void) = ^{
    
    printf("test");
};
int main(int argc, char * argv[]) {
    
    return 0;
}

此源代码通过声明全局变量blk来使用Block语法,我们转化为c++的源代码后会发现其isa指针指向_NSConvreteGlobalBlock,即该Block被分配在数据区。
第二种情况:

int a = 1;
static int b = 2;
int main(int argc, char * argv[]) {
    
    int c = 3;
    
    static int d = 4;
    
    void (^blk)(void) = ^{
        
        NSLog(@"测试数据:a = %d, b = %d, d = %d", a, b, d);
    };
    
    return 0;
}

Block内使用了全局变量,静态全局变量,静态局部变量,唯独没有使用自动变量,因此Block类型就是_NSConvreteGlobalBlock,该Block被分配在数据区。

2. _NSConcreteStackBlock

除了以上两种情况会将Block分配在数据区,其他情况都是讲Block分配在栈区,即Block属于_NSConcreteStackBlock类。

分配在数据区的Block,从变量作用域外也可以通过指针安全地使用。但是分配在栈区的Block,当Block超出变量的作用域范围时,该Block就会被销毁。为了解决这一问题,Block提供了将Block和__block变量从栈区复制到堆区的操作,这样即使Block超过变量的作用域范围,还可以继续访问堆上的Block。

3. _NSConcreteMallocBlock

上面说过_NSConcreteMallocBlock的出现是为了解决当Block分配在栈区时,当Block超出变量的作用域范围时该Block就会被销毁的问题。
在栈区的Block何时复制到堆区多数时候是由编译器决定的。少数时候编译器无法判断的情况是:

向方法或函数的参数中传递Block时。

但是如果在方法或函数中适当的复制了传递过来的参数,那就不必在调用该方法或函数前手动复制了。以下方法或函数不用手动复制:

  • Cocoa框架的方法且方法名中含有usingBlock时。
  • GCD的API。
  • 比如说我们在使用NSArray的enumerateObjectsUsingBlock实例方法以及dispatch_async函数时就不用手动复制。相反,在NSArray类的initWithObjects实例方法上传递Block时需要手动复制。

__block变量存储域

  • 若在一个Block中使用了__block对象,则当该Block从栈复制到堆时使用的所有__block变量也必定配置在栈上。这些__block变量也全部从栈复制到堆上,此时,Block持有该堆上的__block对象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容