您的位置:首页 > 移动开发 > IOS开发

iOS Block源码分析系列(二)————局部变量的截获以及__block的作用和理解

2016-11-13 19:28 585 查看
        上一篇简单介绍了基本Block的内部源码转换以及内部参数结构分析点击打开Block第一篇传送门

         这次介绍一下Block是如何截获自动变量以及__block是什么原理???

直接上代码

#include "stdio.h"
int main(){

int a = 100;
int b = 200;
const char *ch = "b = %d\n";
void (^block)(void) = ^{
printf(ch,b);
};

b = 300;
ch = "value had changed.b = %d\n";
block();
return 0;
}

底层转换代码

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

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *ch;
int b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_ch, int _b, int flags=0) : ch(_ch), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *ch = __cself->ch; // bound by copy
int b = __cself->b; // bound by copy

printf(ch,b);
}

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 a = 100;
int b = 200;
const char *ch = "b = %d\n";
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, ch, b));

b = 300;
ch = "value had changed.b = %d\n";
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}


基本的介绍已经在第一篇介绍完了,结构也非常清楚了,不清楚的朋友点头部的传送门看看,这里看下有什么区别。

1.首先看看Block的内部结构本尊和没有截获的区别

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *ch;
int b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_ch, int _b, int flags=0) : ch(_ch), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
根据main函数里面的Block语法,这里把Block块表达式里面的局部变量作为了这个Block结构体的成员变量追加到了__main_block_impl_0的结构体中

值得注意的是

@ 结构体内声明的成员变量类型与局部变量类型完全相同

@ 语法中没有使用的局部变量(例如咱们这里的a变量)不会被追加

@ 细节需要注意,这里截获的ch是不可修改的,而且捕捉的b只是截获了该局部变量的值而已(下面在将如何截获指针)

2.再来看看该结构体实例化的构造函数以及调用和没有截获的区别

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_ch, int _b, int flags=0) : ch(_ch), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, ch, b));
根据传递给构造函数的参数对由局部变量追加的成员变量进行初始化。

这里传递的参数仅仅只是ch和b的值而已

理解为把Block语法块里面截获的局部变量对__main_block_impl_0的成员追加并且传递赋值

impl.isa = &_NSConcreteStackBlock;

impl.Flags = 0;

impl.FuncPtr = __main_block_func_0;

Desc = &__main_block_desc_0_DATA;

ch = "b=%d\n";

b=200;

由此可以看出,__main_block_impl_0的结构体(即Block)对自动变量进行了截获

3.再来看看最终调用Block的时候和没有截获的区别

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *ch = __cself->ch; // bound by copy
int b = __cself->b; // bound by copy

printf(ch,b);
}
可以看出block()这句代码转换的调用方式没有任何变化

还是转换为一下这句话,而且把本身结构体作为参数进行了传递

(*block-impl.FuncPtr)(block)

现在注意看__main_block_func_0这个C语言的函数

@ 和之前相比这里对传递过来的block结构体参数进行了变量获取__cself->ch 和 __cself->b(这两个变量已经在Block表达式之前进行了声明定义),最终打印出来

总结成一句话

所谓的“截获自动变量”,无非就是在执行Block语法的时候,Block语法所用到的局部变量值被保存到Block的结构体实例当中(即所谓的Block本体)

看的仔细的同学就能有所发现,Block中所捕获的变量就犹如“带有局部变量值的匿名函数”所说,仅仅截获局部变量的值而已,如果在截获的Block里面重写局部变量也不会改变原先所截获的局部变量

block 引用外部对象时候,不是简单的指针引用(浅复制),而是一种重建(深复制)方式(括号内外分别对于基本数据类型和对象分别描述))。所以如果在
block 中对外部对象进行修改,无论是值修改还是指针修改,自然是没有任何效果。


例如

int a = 0;

void (^block)(void) = ^{a = 1};

这样写直接就编译出错了!!!

可以直接看__block_main_block_impl_0的实现上,并不能改写其捕获变量的值,因此直接报错了









简单的分割一下,上面介绍了值的捕获是如何进行的,那么如何解决在Block中保存修改值???

第一种方法:运用C语言中的变量

@ 静态变量

@ 静态全局变量

@ 全局变量

看段简单的代码

#include "stdio.h"
int global_val = 1; // 全局变量
static int static_global_val = 2;//静态全局
int main(){
static int static_val = 3; // 静态局部
void (^block)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;

};
block();
printf("%d\n",global_val);
printf("%d\n",static_global_val);
printf("%d\n",static_val);


这里全局和静态全局问题不大,出了作用于照样能访问,但是静态局部的话如果去掉Static按上面那种写法,就直接报错了,原因就是实现上不能改变捕获的局部变量的值。

没错,应该能想到了,能改变的情况就是直接让Block截获局部变量的指针,看clang的源码

int global_val = 1;
static int static_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

global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;

}

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(){
static int static_val = 3;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
printf("%d\n",global_val);
printf("%d\n",static_global_val);
printf("%d\n",static_val);

}

很简单就能看出区别了

@ 在__main_block_impl_0这个结构体中的追加的成员变量变成了int *_static_val指针了

@ 在结构体实例化函数中__main_block_impl_0参数也变为了&static_val地址了

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy

global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;

}
根据Block语法转换而来的C静态函数,使用static_val的指针进行访问,上面参数可以看出,初始化的时候将静态变量的static_val指针传递给__main_block_impl_0结构体进行追加成员变量初始化并保存,这样做也是对超出作用于使用变量的最简单的方法

第二种方法:就是大家所熟知的__block修饰符的使用

__block int a = 0;

void (^block)(void) = ^{a = 1};

把之前的这段编译错误的代码前面加上static修饰符,再让clang变成源码看下,本以为只是很简单的转变,看来我还是太年轻了,这代码直接暴增啊,暴增啊,这看来又能开几篇博客分析了,越看越多,根本停不下来。。。。。。







struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 1;}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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(){
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
return 0;


分析分析下,一个__block多了那么多代码和结构,真的也是醉了
__block int a = 0;分解如下

__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a,0,sizeof(__Block_byref_a_0),0};

这货竟然加了_block变成了结构体实例,在栈上生成了__block_byref_a_0的结构体实例,a变量初始化为0,这个值也出现在了这个结构体成员变量变量中,意味着该结构体持有相当于原局部变量的成员变量

来来来,看看这货结构

struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};


这个初始化的原结构,里面的最后一个int a就是原局部变量的成员变量

void (^block)(void) = ^{a = 1};分解如下

void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
还是调用__main_block_impl_0的初始化结构体,由于int a加了__block修饰符,那么这个初始化函数传递的不再是&a的地址,而是换成__Block_byref_a_0这个结构体实例的指针进行传递(该结构体其实也是个对象,最后的属性放着需要截获的局部变量)

这里的__main_block_func_0对应的block块函数转换为C

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 1;}

看了两个方案,第一个用C语言的静态变量来解决,那么直接操作静态变量的指针就完事了,但是__block要比这个真的复杂多了。Block的结构体实例持有指向__block变量的 __Block_byref_a_0结构体实例的指针。

_Block_byref_a_0这个结构体实例的成员变量__forwarding持有指向该结构体实例自身的指针。通过__forwarding访问成员变量a

来一张艺术家画的图让大家欣赏下





另外提一点,__block内部的__Block_byhef_val_0这个也是独立的结构体实例,不属于__main_blocl_impl_0结构体中

,这样方便了在多个block中都能访问__block变量

这内部结构越讲越多了。。。。。。下集预告

@ 这里的Block超出其作用变量还可以存在的理由?

@ __forwarding变量存在的理由?

@ 还有刚源码中新增的copy和dispose函数,后面再看吧

还得总结下,不然我也不知道我写了什么。。。。。。

1. 普通的局部变量被Block截获只是单纯的截获其值,并追加到__main_block_impl_0结构体中(无法修改重写)

2.要允许Block修改值有两个方法,第一个就是用C语言的变量,代表就是静态局部变量,第二个就是__block修饰符

3.__block修饰的变量,其实这就是变成了个对象结构体__Block_byref_val_0,初始化__main_block_impl_0的时候追加的是__block生成的结构体指针

4.最终在__main_block_func_0函数中调用__block结构体指针的forwarding指针(这一节指向自己,后面会变)的成员变量a来进行重新赋值

5.其实__block这方法生成的源码,能大致看出来这其实类似OC里面对象的retain和release一系列操作,下一节在看吧

其实大概就明白__block修饰的局部变量,其实也是个结构体实例(对象),并且结构体指针作为参数进行初始化和调用

这里有些结构没细讲,需要的看我第一篇分析 点击打开链接

OK,差不多就这样,后面再介绍吧,跑步去了,觉得哪里理解有问题的可以给我留言














如果我打得过你,我一定打死你
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息