关于Block

192 阅读11分钟

关于block,现在也是烂大街的技术点了,随便一搜,什么样式的都有,加上前几天在一个技术交流群里面被一个大佬教育了一番,就按照自己的方式重新学习了一下block。 (我本以为我懂block,c语言函数嘛,最多还是个匿名函数,正经点回答那不就是个带有自动变量的匿名函数嘛,结果人家的问题是-block如何封装函数调用跟函数调用环境的)

为了重新认识block,我选择我自己最不擅长的方式--学习源码 以下便是笔者的学习过程,特意记录一下,方便日后查看。

一般的理解,本质为c语言函数---带有自动变量值的匿名函数 例子:

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

使用clang -rewrite-objc 文件名 将c语言代码转换成底层实现代码 按文件中的顺序依次如下

62行

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

isa指针指向其父类 flags标志 reserved后续版本升级所需区域 funcpstr函数指针

最底下部分

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\n");
    }
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)};

desc结构体的内容也是今后版本升级所需区域以及block的大小 __main_block_desc_0_DATA作为_main_block_impl_0结构体实例构造过程中的第二个参数

构造函数的调用如下

 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结构体指针类型的变量blk

继续分析__main_block_impl_0结构体实例的构造参数 __main_block_func_0 第一个参数是由Block语法转换的c语言函数指针 __main_block_desc_0_DATA 第二个参数是作为静态全局变量初始化的__main_block_desc_0结构体实例指针

blk()调用如下

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

blk()去掉转换部分 (*blk->impl.FuncPtr)(blk); 这个其实就是简单的使用函数指针调用函数

关于impl.isa = &_NSConcreteStackBlock;

将Block指针赋值给Block的结构体成员变量isa。首先要知道oc中的类和对象的实质,在oc的runtime.h文件中是如下声明的:

typedef struct objc_object{
   Class isa
} *id;
typedef struct objc_class *Class;
struct objc_class{
   Class isa;
};

这两个结构体都是最基本的结构体,假如有个Person类,并且声明了两个int类型的成员变量val0,val1,基于objc_object,那么Person的结构体将会是:

struct Person{
    Class isa;
    int val0;
    int val1;
}

可以看到,实例变量被直接声明为对象的结构体成员

各类的结构体就是基于objc_class结构体的class_t结构体,class_t声明如下

struct class_t {
    struct class_t *isa;
    struct class_t *superclass;
    Cache cache;
    IMP *vtable;
    uintptr_t data_NEVER_USE;
};

class_t结构体实例持有声明的成员变量,方法的名称,方法的实现(函数指针),属性以及父类的指针,到此基本可以理解oc的类与对象的实质

回头再看Block的结构体

struct __main_block_impl_0 {
    void *isa;
    int Flags;
    int Reverse;
    void *Funcptr;
    struct __main_block_desc_0* Desc;
}

__main_block_impl_0结构体相当于基于objc_object结构体的oc类对象的结构体,其中对成员变量isa初始化就是 isa = &_NSConcreteStackBlock; 也就是说,_NSConcreteStackBlock相当于class_t结构体实例,在将Block作为oc对象处理时,关于该类的信息放置于_NSConcreteStackBlock中。

关于截获自动变量值

    int a = 3;//注意下面所有出现a的部分
    void (^blk2)(void) = ^{
        printf("%d",a);
    };
    blk2();

clang转换后:

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  int a;    //注意这里
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
  int a = __cself->a; // bound by copy
//__cself->a的值为3 在__main_block_impl_1构造中被初始化
        printf("%d",a);
    }
static struct __main_block_desc_1 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};

通过转换 可以看到,Block语法表达式中使用的自动变量被作为成员变量追加到__main_block_impl_1结构体中,需要注意的是,仅仅在Block语法表达式中使用到的自动变量才会被追加进去,Block的自动变量截获只针对Block中使用的自动变量(如果是id类型对象,只使用而不是赋值,也会被追加进去) 另外,在函数调用实现中,生成了临时变量a,并将外部捕获的a(也就是block结构体的成员变量a)赋值给临时变量,随后进行使用(打印),这也是为什么在block体后面修改a不会影响打印结果的原因

构造函数以及调用部分如下

int a = 3;
    void (*blk2)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)blk2)->FuncPtr)((__block_impl *)blk2);

这里也可以清楚看到a被传入到结构体构造过程中,使用a来初始化__main_block_impl_1结构体实例,由此可知,自动变量被截获--Block语法表达式所使用的自动变量值被保存在Block的结构体实例(即Block自身)中

__block修饰符

block体内修改自动变量值,编译无法通过,在给自动变量加上__block修饰符后便可以实现改写操作,或者将自动变量声明为 静态变量/静态全局变量/全局变量

假如分别声明

int global_val = 1;
static int static_globle_val = 2;
static int static_val = 3;

然后

void(^blk)(void) = ^{
     global_val *= 1;
     static_global_val *= 2;
     static_val *= 3;
};

经转换后 只有static_val 会被Block截获,表现与自动变量一致

static void __main_block_func_0(struct __main_block_impl_0 *__cself){
    int *static_val = __cself->static_val;
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *= 3;
}

使用静态变量static_val的指针对其进行访问,将静态变量static_val的指针传递给__main_block_impl_0结构体的构造函数并保存。这是超出作用域使用变量的最简单方法

接着使用__block改写自动变量

__block int b = 10;
    void (^blk3)(void) = ^{
        b = 20;
    };
    blk3();
    printf("%d",b);

转换后

block结构体结构不变

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
__block int b = 10;

转换成

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};

可以看到,自动变量b被__block修饰后,不再是以单纯的int类型保存在__main_block_impl_2结构体中,而是以__Block_byref_b_0结构体实例保存的,这就意味着该结构体持有相当于原自动变量的成员变量

block结构体的构造过程如下

struct __main_block_impl_2 {
  struct __block_impl impl;
  struct __main_block_desc_2* Desc;
  __Block_byref_b_0 *b; // by ref 不再是简单的int类型成员变量 而是结构体指针,且构造函数的参数也是结构体指针
  __main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, __Block_byref_b_0 *_b, int flags=0) : b(_b->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

^{b=20;};转换成了

static void __main_block_func_2(struct __main_block_impl_2 *__cself) {
  __Block_byref_b_0 *b = __cself->b; // bound by ref

        (b->__forwarding->b) = 20;
    }

Block的__main_block_impl_2结构体实例变量持有__block变量的__Block_byref_b_0结构体实例的指针 也就是说 b的指针指向__Block_byref_b_0 __Block_byref_b_0结构体的实例变量__forwarding的指针指向该实例变量自身

通过__forwarding访问成员变量b,这个b是该实例变量的成员变量,它相当于原自动变量,修改__Block_byref_b_0结构体的实例变量中__forwarding所指向的成员变量的值也就达到修改了原自动变量的值

Block超出变量作用域可存在的原因

Block分为三种,记述全局变量的地方有Block语法时或者Block表达式中不使用应截获的自动变量时,该Block为全局Block,Block用结构体的成员变量isa的初始化为 impl.isa = &_NSConcreteGlobalBlock 全局Block配置在数据区域 除去以上情况下的Block,其它都是_NSConcreteStactBlock,配置在栈上

配置在全局变量上的Block,从变量作用域外也可以通过指针安全使用,但是设置在栈上的Block,如果所属的变量作用域结束,该Block就被废弃,由于__block变量也配置在栈上,作用域结束后也会被废弃,废弃后自然就不能再使用了,为了解决这个问题,可以将栈上的Block复制到堆上面,这样即使Block语法记述的变量作用域结束,堆上的Block还可以继续使用,isa初始化为 impl.isa = &_NSConcreteMallocBlock

关于如何复制

1.当Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码 blk tmp =__main_block_impl_0(构造参数) 构造过程中会调用

tmp = objc_retainBlock(tmp);  
return objc_autoreleaseReturnValue(tmp)

objc_retainBlock其实就是_Block_copy函数 objc_autoreleaseReturnValue 将复制到堆上后的Block当做oc对象注册到自动释放池当中 然后返回该对象

2.Block不是作为函数的返回值返回时,也就是当函数参数传递的时候,需要手动调用copy方法,将Block从栈上复制到堆上

编译器不能判断的情况,向方法或函数的参数中传递Block时

如果不进行copy操作,程序会崩溃,这是因为getBlockArray函数执行完后,配置在栈上的block被废弃

其实也可以不让编译器判断,所有地方进行copy操作,但是拷贝操作相当消耗CPU

系统带有usingBlock的api以及GCD是不需要手动复制的

以上copy操作都是将block从栈复制到堆,如果block本来就处于堆区域,再进行copy操作,会导致block的引用计数增加,如果是全局block进行copy,则什么都不会做

block的复制操作也是符合oc对象的引用计数式内存管理思维方式

复制的时机

1.调用block的copy方法

2.Block作为函数返回值的时候

3.将block赋值给附有__strong修饰符id类型的类或Block类型成员变量时候

4.在系统api带有usingblock和GCD的api中传递block的时候

__forwarding存在的原因

不管是栈block还是堆block,都能够正确访问该_block变量

通过该功能,就可以实现无论是在block语法中,block语法外使用__block变量,还是__block变量配置在栈或者堆,都可以正确访问同一个__block变量

上述使用__block改写自动变量还遗留了部分代码如下: 其实剩余部分陌生代码也是可以解释超出作用域能存在的原因—个人理解

在oc中,c语言结构体不能含有附有__strong修饰符的变量,因为编译器不知道何时进行c语言结构体的初始化和废弃操作,不能很好的管理内存,但是oc的运行时库可以准确把握block从栈复制到堆以及堆上block被废弃的时机,为此需要在__main_block_desc_0结构体中增加成员变量copydispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和__main_block_dispose_0函数。

static void __main_block_copy_2(struct __main_block_impl_2*dst, struct __main_block_impl_2*src) {_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}

block_object_assign函数调用相当于retain,将对象赋值在对象类型的结构体成员变量中 如果block捕获的自动变量为__strong修饰的id对象类型,注释部分应该是BLOCK_FIELD_IS_OBJECT

static void __main_block_dispose_2(struct __main_block_impl_2*src) {_Block_object_dispose((void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}

block_object_dispose函数相当于release,释放对象类型的结构体成员变量中的对象

static struct __main_block_desc_2 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_2*, struct __main_block_impl_2*);
  void (*dispose)(struct __main_block_impl_2*);
} __main_block_desc_2_DATA = { 0, sizeof(struct __main_block_impl_2), __main_block_copy_2, __main_block_dispose_2};
__attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};
    void (*blk3)(void) = ((void (*)())&__main_block_impl_2((void *)__main_block_func_2, &__main_block_desc_2_DATA, (__Block_byref_b_0 *)&b, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk3)->FuncPtr)((__block_impl *)blk3);
    printf("%d",(b.__forwarding->b));

到此为止,block的运行机制应该有个比较明朗的认识了,而block如何封装函数调用,其实也就是block结构体构造过程中的第一个参数—函数指针,我们在调用block的时候,底层是通过c语言函数指针进行函数的调用

 __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;

此处构造函数中的*fp是作为第一个参数传递进来并且被block结构体保存了

函数调用环境的封装就是这个c语言函数的入参与出参,block通过变量截获得到了

结论

1.Block是封装了函数调用与函数调用环境的C语言函数

2.Block能够捕获自动变量的瞬时值是因为自动变量的瞬时值被当做block结构体的成员变量保存起来了,构造函数在初始化的时候将瞬时值赋值给了该成员变量

3.__block修饰的变量能够实现改写是因为被修饰的变量转换成了结构体(block-byref)并被声明为block结构体的成员变量,block-byref结构体中保存了原自动变量的值,原自动变量的指针指向block-byref结构体,block-byref结构体的__forwarding指向block-byref本身,通过成员变量__forwarding访问使用值

4.ARC环境下,block属性关键字无论是copy还是strong都可以,使用copy是因为历史遗留问题,使用strong也可以是因为编译器已经能够正确判断block的内存管理

最后,如果本文侥幸入了你的法眼,如有描述不当之处希望能够不吝赐教,要是能帮到你加深对block的理解就是我的额外收获了,感谢大佬观看,欢迎批评指正

参考

《Object-C高级编程》