Blocks原理探究

1,025 阅读11分钟

Blocks可以用一句话来概括:带有自动变量的匿名函数。关于Blocks的语法和用法,本文不在过度赘述。而是聚集于Blocks的本质到底是什么?他是怎么实现的?

Block结构与实质

Block实际上是C语言的扩充,也就是说,Block语法源代码是会被编译为普通的C语言源代码的。通过clang可以将其转换为我们可读代码,例如下面代码:

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

通过clang转换后的代码:

//block实现结构体
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//block结构体
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;
  }
};

//block代码块中的实现
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("hello world");
}

//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, const char * argv[]) {
//block实现
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//block调用
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

简单的几行代码转换后竟然增加了这么多,但是仔细看,其实并不难理解。可以分为两部分:实现block、调用block。

实现block
转换后是通过下面代码实现block的:

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

它调用了__main_block_impl_0结构体来实现,而该结构体又是分别包含__block_impl结构体和__main_block_desc_0结构体2个成员变量

// impl结构体
struct __block_impl {
  void *isa;  // 存储位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 按位表示一些 block 的附加信息
  int Reserved;  // 保留变量
  void *FuncPtr;  // 函数指针,指向 Block 要执行的函数,即__main_block_func_0
};

// Desc结构体
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)};

再来看__main_block_impl_0结构体的构造函数:

__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_desc_0结构体实例指针,第三个参数flags有默认值0。重点看第一个参数,他其实就是block语法生成的block函数:

^{ printf("hello world"); };

经过转换后__main_block_func_0函数指针:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("hello world");
    }

再来重点看__main_block_impl_0构造函数的一行代码:

impl.isa = &_NSConcreteStackBlock;

将_NSConcreteStackBlock地址赋值给isa。我们再回顾下objc_object的实现,其也包含isa指针。__main_block_impl_0结构体相当于基于objc_object结构体的oc类对象的几多题。其成员变量isa通过_NSConcreteStackBlock初始化。即_NSConcreteStackBlock相当于class_t结构体实例。在将block作为对象处理时,其类信息放置于_NSConcreteStackBlock中。

调用block
调用block就相对简单多了。将第一步生成的block作为参数传入FucPtr(也即_main_block_func_0函数),就能访问block实现位置的上下文。

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

总结:block实际上是通过block_impl结构体实现的,而该结构体的首地址是isa,因此在objc中,block实际上就算是对象。

捕获外部变量

通过上面我们已经理解block匿名函数的本质了,那么带有自动变量又是指什么呢?先看下面这段代码:

int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10
    id array = [[NSMutableArray alloc] init];
    void (^blk)(void) = ^{
        printf("val = %d", val);
        [array addObject:@"obj"];
     	printf("%lu", (unsigned long)[array count]);
    };
    blk();
    return 0;
}

通过clang转换后的代码,我们主要来看其中的不同之处,首先来看__main_block_impl_0结构体:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;//编译时就自动生成了相应的变量
  __strong id array; //注意这里
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, __strong id _array, int flags=0) : val(_val) , array(_array) {
    impl.isa = &_NSConcreteStackBlock;//block的isa默认是stackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

首先来看__main_block_impl_0结构体内申明的成员变量类型与自动截取的变量类型完全相同。block表达式未使用的自动变量不会追加到结构中,例如dmy。另外,该结构体的构造函数中,加入了自动变量的初始化。 再来看看匿名函数的实现:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int val = __cself->val; // bound by copy 值拷贝,即 val = 10,此时的a与传入的__cself的val并不是同一个
  printf("val = %d", val);
  __strong id array = __cself->array; // bound by copy
  [array addObject:@"obj"];
  printf("%lu", (unsigned long)[array count]);
}

同时,转换后的代码里还多了下面这些代码,他们是用来干什么的?我们暂且不表,将在__block说明符章节中做详细说明:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{
        _Block_object_assign((void*)&dst->array, 
                                     (void*)src->array,       
                                     3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src)
 {  
        _Block_object_dispose((void*)src->array, 
                               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
};

通过以上代码我们可以知道:

  • 变量截获的就是它的值,对象类型的自动变量,本身是指向对象的指针,指针的值是地址
  • 对象类型的类型连同其修饰符一起截获
  • 自动变量值被保存到Block的结构体实例中

block的存储域

前面我们说到block也是oc对象,按照isa对应的class_t信息来分类,block可以分为以下三种:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock
    通过命名我们可以猜测出,他们对应的block对象分别存储在全局、栈、堆上

_NSConcreteGlobalBlock
block是_NSConcreteGlobalBlock的情况有以下两种:

  • 全局block:
void (^glo_blk)(void) = ^{
    NSLog(@"global");
};

int main(int argc, const char * argv[]) {
    glo_blk();
    NSLog(@"%@",[glo_blk class]);
}
  • 在函数栈上创建但没有截获自动变量:
int glo_a = 1;
static int sglo_b =2;
int main(int argc, const char * argv[]) {
    void (^glo_blk1)(void) = ^{//没有使用任何外部变量
        NSLog(@"glo_blk1");
    };
    glo_blk1();
    NSLog(@"glo_blk1 : %@",[glo_blk1 class]);
    
    static int c = 3;
    void(^glo_blk2)(void) = ^() {//只用到了静态变量、全局变量、静态全局变量
        NSLog(@"glo_a = %d,sglo_b = %d,c = %d",glo_a,sglo_b,c);
    };
    glo_blk2();
    NSLog(@"glo_blk2 : %@",[glo_blk2 class]);
}

_NSConcreteStackBlock和_NSConcreteMallocBlock

  • _NSConcreteStackBlock是设置在栈上的block对象,生命周期由系统控制的,一旦所属作用域结束,就被系统销毁了。
  • _NSConcreteMallocBlock是设置在堆上的block对象,生命周期由程序员控制的。

ARC有效时,以下几种情况,编译器会进行判断,自动将栈上的Block复制到堆上:

  • 调用Block的copy方法
  • 将Block作为函数返回值时
  • 将Block赋值给__strong修饰的变量或Block类型成员变量时
  • 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时 例如下面这段代码:
    int num = 10;
    //输出_NSConcreteStackBlock
    NSLog(@"%@",[^{
        NSLog(@"%d",num);
    } class]);


    void (^block)(void) = ^{
        NSLog(@"%d",num);
    };
	//输出_NSConcreteMallocBlock
    NSLog(@"%@",[block class]);
    

除此之外,都推荐使用block的copy实例方法把block复制到堆上。例如下面这个例子:

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            ^{NSLog(@"blk1:%d", val);}, nil];
}
int main(int argc, char * argv[]) {
    id obj = getBlockArray();
    void (^blk)(void) = [obj objectAtIndex:1];
    blk();
    return 0;
}

运行程序崩溃,因为NSArray内的block类型为_NSConcreteStackBlock,getBlockArray函数执行完成后,就被自动释放废弃了,再执行[obj objectAtIndex:1]时,就发生异常。
为了解决上述问题,通过手动copy复制到堆上即可:

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"blk0:%d", val) ;} copy],
            [^{NSLog(@"blk1:%d", val);} copy], nil];
}

__block说明符

再回到截获自动变量值的例子,假如我们在block中试图改变自动变量val。

int main(int argc, const char * argv[]) {
	NSMutableArray *array = nil;
    int val = 10
    void (^blk)(void) = ^{
    	val = 1;
        array = [NSMutableArray array];
        printf("val = %d", val);
    };
    blk();
    return 0;
}

结果编译器报错。因为不能改写被截获的自动变量的值,个人猜测,如果在block内修改自动变量的值是可行的,那么修改的应该是结构体内的临时变量,与自动变量互不影响。这很容易引起开发者犯错,为了避免这种情况,编译器不允许在block中修改自动变量的值,否则报错。
为了解决这个问题有两种方法。第一种,将截获的自动变量改写成下列类型:

  • 静态局部变量
  • 静态全局变量
  • 全局变量 静态全局变量、全局变量这两种外部变量,因为其作用域是全局的,在block内可以直接访问。所以不需要截获。 静态局部变量本身在block语法的函数外,他是怎么做到可以修改值的,先来看下面代码:
int main(int argc, const char * argv[]) {
    statc int val = 10
    void (^blk)(void) = ^{
    	val = 1;
    };
    blk();
    return 0;
}

转化后:

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;//block的isa默认是stackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • 从结构体成员变量int * static_val看出,block截获静态变量为结构体成员变量,截获的是静态变量的指针。
  • 这看起来似乎和 自动变量是指向对象的指针 的情况差不多,但不同的是,在block内修改静态变量的值是通过修改指针所指变量的来做的:(* static_val) = 1。而这也是为什么block内能修改自动变量的原因。 但是实际使用中,我们很少采用这种方案。因为block中可以存放超过变量作用域的自动变量,而当使用超过作用域的静态局部变量时,无法通过指针访问。
    为了解决此类问题,我们采用第二种方案"__block说明符",他又是怎么实现的呢?

__block的实现

我们将上述代码改为:

int main(int argc, const char * argv[]) {
    __block int val = 10
    void (^blk)(void) = ^{
    	val = 1printf("val = %d", val);
    };
    blk();
    return 0;
}

转换后的代码:

struct __Block_byref_val_0 {
  void *__isa;
 __Block_byref_i_0 *__forwarding; // 注意这里!!!!!
 int __flags;
 int __size;
 int val;
};

/ * Block结构体 */
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // 注意这里!!!!!
构造函数
__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;
  }
};
/ * Block方法 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_val_0 *val = __cself->val; // 注意这里!!!!!
     (val->__forwarding->val) = 1;// 注意这里!!!!!
}

//捕获的变量的copy和release
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*/);}

//block的描述结构体
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[]) {
	/ * __block int val = 0; 转换后代码*/
  	__Block_byref_val_0 val = {
  						0, //__isa
                        (__Block_byref_val_0 *)&val,  // __forwarding 注意这里!!!!
                        0,  // flag
                        sizeof(__Block_byref_val_0), // size
                        10 // 变量i
                      };
                      
	blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DAYA, &val, 0x22000000);
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
	return 0;
}

只是在自动变量上附加了__block说明符,源代码就急剧增加。我们先来看看__block变量val是怎么转换的?

/ * __block int val = 0; 转换后代码*/
  	__Block_byref_val_0 val = {
  						0, //__isa
                        (__Block_byref_val_0 *)&val,  // __forwarding 注意这里!!!!
                        0,  // flag
                        sizeof(__Block_byref_val_0), // size
                        10 // 变量i
                      };

我们发现他竟然变为了结构体实例,用__block修饰的变量变成了__Block_byref_val_0结构体类型,其定义如下:

struct __Block_byref_val_0 {
  void *__isa;
 __Block_byref_i_0 *__forwarding; // 注意这里!!!!!
 int __flags;
 int __size;
 int val;
};

通过block结构体的初始化,我们可以看出,block捕获的实际是__Block_byref_val_0结构体的地址:

blk = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DAYA, &val, 0x22000000);

再来看看__block变量的赋值代码又是如何实现的?

//^{val = 1;}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_val_0 *val = __cself->val; // 注意这里!!!!!
     (val->__forwarding->val) = 1;// 注意这里!!!!!
}
  • 取到指向__Block_byref_val_0结构体类型的变量val的指针
  • 通过__forwarding访问到自动变量val,对其进行赋值操作。 那么问题来了,他是如何解决局部自动变量超出作用域后,还能正常使用的问题?为什么要设计__forwarding?

block变量的内存管理

捕获对象类型的变量和使用__block 修饰自动变量时,都在clang转换的代码中,看到了这样两个函数。简单来说他们都是用来做block结构体变量的复制和释放的。

//捕获的变量的copy和release
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*/);}

以__block修饰的自动变量具体,栈block通过copy复制到了了堆上。此时,block使用到的__block变量也会被复制到堆上并被block持有。如果是多个block使用了同一个__block变量,那么,有多少个block被复制到堆上,堆上的__block变量就被多少个block持有。当__block变量没有被任何block持有时(block被废弃了),它就会被释放。

栈上__block变量被复制到堆上后,会将成员变量__forwarding指针从指向自己换成指向堆上的__block,而堆上__block的__forwarding才是指向自己。

这样,不管__block变量是在栈上还是在堆上,都可以通过__forwarding来访问到变量值。

总结:

  • block捕获__block变量,捕获的是对应结构体的变量的地址,该结构体也哟isa指针,也可以理解为对象。
  • 当block复制到堆上,block使用到的__block变量也会被复制到堆上并被block持有。

最后,简单提下block循环引用,其产生循环引用的原理根普通循环引用一样,解决的办法也一样:

  • 使用弱引用,避免产生相互循环引用
  • 在合适的时机手动断环 不在过多介绍。

参考文章:
Objective-c高级编程-ios与OS X多线程和内存管理
iOS Block原理探究以及循环引用的问题