OC中Block原理解析

原创文章转载请注明出处,谢谢


这段时间重新回顾了一下Block的知识,由于只要讲原理方面的知识,所以关于Block的用法就不做介绍了,不清楚的同学请自行补充。

Block介绍

Block是C语言的扩充,即带有自动变量的匿名函数,它和普通变量一样,可以作为自动变量,函数参数,静态变量,全局变量。


Block中截获自动变量值

首先我们要理解自动变量是什么?我们观察下面一段代码,一个非常简单的Block,打印了val的变量。

int val = 1;
void (^blk)(void) = ^{
    printf("%d\n", val);
};
val = 2;
blk();

/*output*/
1

在上面的源码中,Block表达式使用了之前声明的自动变量val,虽然我们在之后调用block之前有修改val的值,但是打印出来的结果还是之前的旧值,关于这点大家应该都知道,但是至于原因是为什么,其实就是Block保存了val的瞬间值,具体说明我们需要通过了解Block的实现原理才清楚。


Block的定义

通过将代码转换成C++的源码我们可以发现Block的定义。

int main(int argc, const char * argv[]) {
    int val = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
    val = 2;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我们发现转换后的代码主要包括以下几个结构体:

// __main_block_impl_0就是block的定义,它也是由几个变量和构造函数组成的。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val; // 自动变量备份
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
struct __block_impl {
  void *isa;     // 地址指向
  int Flags;     // 某些标志
  int Reserved;  // 关于版本升级所需的区域
  void *FuncPtr; //具体block函数的实现
};
static struct __main_block_desc_0 {
  size_t reserved;   // 关于版本升级所需的区域
  size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// 该函数的参数__cself相当于C++实例方法中的this,也就是OC中的self,指向Block自身;
// 通过读取自身保存的val,最后输出结果
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy
  printf("%d\n", val);
}

通过上面的结构我们可以发现Block的具体结构,我们会过头来继续看之前的自动截获变量的值不会变的问题,我们来看一下转换后的函数调用。

int val = 1;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 2;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

我们的block其实是传入了val的值并不是指针,block中备份了一个新的值,所以当val的值再之后再被修改的时候,block中的值不会改变。

所以Block的结构其实是总结就是下图 :

OC中Block原理解析_第1张图片
1.pic.jpg

Block中改写自动变量值

好,如果我们想改变自动变量的值呢?脑补一下是不是可以用以下几种方式来改变:

  • 静态局部变量
  • 静态全局变量
  • 全局变量

因为静态变量都是存在数据段,并不是栈里,数据并不会离开作用域而销毁,所以我们来验证一下,定义如下三个变量:

static int static_global_val = 1;
int global_val = 2;

int main(int argc, const char * argv[]) {
    static int static_val = 3;
    void (^blk)(void) = ^{
        static_global_val++;
        global_val++;
        static_val++;
        printf("%d\n", static_global_val);
        printf("%d\n", global_val);
        printf("%d\n", static_val);
    };
    blk();
    return 0;
}
/*output*/
2
3
4

果然采用这种方式是可以改变自动变量的值的,我们来看一下具体的block结构有什么变化:

static int static_global_val = 1;
int global_val = 2;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy
  static_global_val++;
  global_val++;
  (*static_val)++;
  printf("%d\n", static_global_val);
  printf("%d\n", global_val);
  printf("%d\n", (*static_val));
}

int main(int argc, const char * argv[]) {
    static int static_val = 3;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我们发现对于全局变量和全局静态变量来讲没有什么改变,它们仍然是全局变量,但对于静态局部变量来说,它则是传入了静态局部变量的指针,block通过保存静态变量的指针,来在block中修改自动变量的值。

那我们是不是就可以通过静态局部变量这种方式在block中修改变量的值呢?答案是最好不要,因为这种情况下block的代码会存放在数据段中,详细的说明我们下面再说,主要会讲到MRC和ARC在block的区别,这里主要是为了引出我们的__block关键字。


Block中的__block说明符

关于__block说明符大家肯定不会陌生了,它的出现允许我们能在block中修改自动变量的值,只要我们在定义变量的时候在前面加上block说明符就可以了,那么它的原理是什么呢?我们还是通过一段代码来看一下结果:

 __block int val = 1;
 void (^blk)(void) = ^{
     val++;
     printf("%d\n", val);
 };
 blk();
 
 /*output*/
 2

将上面的源码转化成C++,我们就可以发现这个时候的Block结构和之前不使用__block有很明显的区别了。

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)++;
  printf("%d\n", (val->__forwarding->val));
}
    
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, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = 
        {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
        
    void (*blk)(void) = 
        ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
        
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

我们仔细回过头来看,其实这个结构无非多了一个结构__Block_byref_val_0,它正是我们声明的val;

// 构造函数
{
 (void*)0,
 (__Block_byref_val_0 *)&val, 
 0, 
 sizeof(__Block_byref_val_0), 
 1
}

// _block结构
struct __Block_byref_val_0 {
  void *__isa;   // 变量存放的区域
  __Block_byref_val_0 *__forwarding;  //指向自身
 int __flags; // 标志位
 int __size; //  结构大小
 int val;    // 变量的实际值
};

于是我们可以明白为什么声明__block的变量可以在block中修改自动变量了,因为变量本身是一个结构体了,我们存放指针的方式就可以修改实际的值了。

所以带有__block说明符的Block的结构其实是总结就是下图 :

OC中Block原理解析_第2张图片
2.pic.jpg

好,到现在为止我们还有很多的问题没有解决,让我们回过头来梳理一下:

  • 为什么__block其实只是定义了一个结构体,我们只是通过指针来修改它的值,那么为什么不直接使用一个变量的指针来修改值就好了,这样不是更佳简单?
  • 为什么我们不推荐静态局部变量的形式来修改我们的自动变量呢?
  • __Block_byref_val_0结构中的forwarding是用来干什么的呢?
  • 还有就是我们__block_impl中的isa指针,是指向什么?NSConcreteStackBlock又是什么?
  • 当我们使用了__block以后,在main_block_desc_0中就多了main_block_copy_0和main_block_dispose_0两个函数,是用来干什么的?

接下来的一部分就来讲关于这些问题的解释。


Block的存储域

首先要说明的就是我们的变量,它们存放的地方可以是栈区,数据段,以及堆区,所以Block其实也一样,它也可以存放在这些地方,主要有以下几种:

  • _NSConcreteStackBlock // 存储在栈上
  • _NSConcreteGlobalBlock // 存储在数据段
  • _NSConcreteMallocBlock // 存储在堆上

我们最早的一段代码,isa指向的就是存放在栈上,但是并不是所有的Block都是存放在栈上的。

存放在数据段(全局区)的Block情况

1 使用了静态或者全局变量的时候,block实际上是存放在全局区的,我们来验证一下:

// 静态变量
// 通过po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>

static int val = 1;
void (^blk)(void) = ^{
    val++;
    printf("%d\n", val);
};
blk();

// 全局变量也是一样,所以不做展示

但是这里有一点值得注意的是,通过clang转换出来的C++代码,好想一直都是显示_NSConcreteStackBlock,这个其实是有问题的,可能是编译器的优化什么的,这个我们不做考虑。所以我们并不推荐使用局部静态变量来修改自动变量的值,因为它会把我们的block全部存放在全局区中,这显然是不太好的。

2 Block语法的表达式中不使用截获的自动变量,也就是不使用外部变量,block也是存放在全局区的。

// 不使用外部变量
// 通过po blk
// (lldb) po blk
// <__NSGlobalBlock__: 0x100001060>

void (^blk)(void) = ^{
    printf("Hello\n");
};
blk();

好,以上两种方式,其实我们的block并不会存放在栈区,而是存放在全局区,也就是数据段里面。

存放在堆区的Block情况

为什么我们有时候要把block存放在堆上,而不在栈上呢?默认我们都是存放在栈上的,但是这有一个问题,设置在栈上的Block一但作用域结束就会被自动释放,由于__block变量也是在栈上,同样在作用域结束的同时,变量同样会被释放。

所以这个时候我们就需要将block从栈上复制到堆上来解决这个问题。那么什么情况下我们需要这么做呢?其实讲这部分的内容是需要区分环境来说明的,也就是在ARC和MRC这两种环境下是有区别的。首先我们来看如下的一段代码来说明这个问题:

typedef void (^blk)(void);

blk getBlock() {
    __block int val = 1;
    blk b = ^{
      printf("%d\n", ++val);
    };
    return b;
}

 blk b = getBlock();
 b();
 
 /*MRC output*/
 1606416289
 po b
 <__NSStackBlock__: 0x7fff5fbff7a0>
 
 /*ARC output*/
 2
 po b
 <__NSMallocBlock__: 0x100700050>

我们发现MRC和ARC下的结果是不一样的,明显ARC下是对的,MRC下的堆栈已经不对了,但是为什么会发生这种情况呢?其实就是我上面讲的__block变量在离开作用域的时候已经被释放了,所以这个时候的val已经是一个野指针了,结果当然不对。那么为什么ARC结果就是对的呢?

原因就在于ARC情况下,编译器主动的把栈上的block做了一个备份到堆上。所以我们还可以正确的访问变量。
那么我们之前的__forwarding为什么会指向自身就可以理解了,其实这个时候栈上的forwarding其实是去指向堆中的forwarding,而堆中的forwarding指向的还是自己,所以这样就能保证我们访问的就是同一个变量,就如下图所示:

OC中Block原理解析_第3张图片
3.pic_hd.jpg

那我们我们如何手动的去把block从栈上备份到堆上呢?答案就是使用copy。我们把原来在MRC下执行的代码做修改:

typedef void (^blk)(void);

blk getBlock() {
    __block int val = 1;
    blk b = ^{
      printf("%d\n", ++val);
    };
    return [b copy];
}

 blk b = getBlock();
 b();
 
 /*MRC output*/
 2
 po b
 <__NSMallocBlock__: 0x100700050>

这个时候我们发现block已经备份到堆上去了。

在ARC下,通常讲Block作为返回值的时候,编译器会自动加上copy,也就是自动生成复制到堆上的代码。但是也有些例外的情况,连ARC也无法判断,这个时候ARC出于保守估计是不会主动加上copy的,因为从block从栈上复制到堆上是非常吃CPU的,如果Block在栈上就已经做够使用了,那么我们其实就不需要将它拷贝到堆上了。

主要有以下几种情况编译器是不能判断的,所以不会加上copy:

  • 向方法或者函数的参数中传递Block的时候。(需要我们自己手动使用copy)
  • Cocoa框架的方法且方法中含有usingBlock等时。(不需要我们手动copy)
  • GCD的API中传递Block时。(不需要我们手动copy)

但是这里我强烈的怀疑第一种情况在xcode7中可能已经被优化了,我们看下面这个经典的例子:

id getBlockArray() {
    int val = 10;
    return [NSArray arrayWithObjects:^{NSLog(@"a %d", val);},
                                     ^{NSLog(@"b %d", val);},
                                     nil];
}

id datas = getBlockArray();
blk a = (blk)[datas objectAtIndex:0];
a();

理论上getBlockArray获取到的Block无论在MRC下还是ARC下都应该是在栈区,这段代码应该是不对的,网上也有人说是不对的,但是它们都是使用了xcode5,我在xcode7下,在ARC的情况下竟然可以运行,而且结果正确,所以我怀疑现在已经没问题了。不过我们还是要记住,哪些情况是需要我们主动把block从栈区备份到堆区的才可以。

最后总结一下什么时候栈上的block会复制到堆上呢?(ARC下)

  • 调用Block的copy函数的时候
  • Block作为函数值返回的时候
  • 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量的时候
  • 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时

还有就是除了以下几种情况外,我们都应该主动调用Block的copy方法,其实有点重复。

  • Block作为函数值返回的时候
  • 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量的时候
  • 在方法名字中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时

总结

感觉这次讲的有些乱了,我并不是从MRC时代开始写OC的,所以直接从ARC开始让我不知道很多以前MRC时代的东西,这点让我感觉Apple做的其实不好,ARC对于开发虽然方便了,但是也容易让开发人员变的不了解很多东西的原理了。

你可能感兴趣的:(OC中Block原理解析)