利用Clang探究__block的本质

前言

上一篇文章利用Clang探究block的本质得出的结论是block的本质是一个结构体对象,该对象包含调用block时要执行的函数指针。当我们在Objective-C层面调用block的时候,底层就可以通过调用block对应的C++结构体对象的函数指针来实现同等操作。

我们知道如果在block内部访问(此处的访问是指setter而非getter)block函数体外部的局部变量时,编译期会报错,如下图:


所以我们通常的解决方案是在被访问的局部变量前面使用__block说明符对变量进行修饰,如下:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
__block int var = 10;

self.block = ^{
var = 20;
};
self.block();
NSLog(@"var == %d",var);
}

所以前辈们总结出了结论:

默认情况下,不允许在block内部修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址,也就是外部局部变量(所以不包含全局变量和静态变量)。__block所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

那么问题来了,为什么被__block说明符修饰的变量就可以在block函数体内部访问呢?__block底层究竟对被修饰的变量做了什么呢?__block是如何将“外部变量”从栈中转移到堆中?这将是本篇文章探究的问题。

所以,本篇文章主要研究__block说明符底层作用以及__block所代表的深层含义。

源码分析

和上一篇文章一样,我们还是在main.m文件中定义一个main函数,并使用clang对这个main函数进行rewrite,使其转换为C++源码,通过C++源码窥探__block底层实现。main.m文件内容如下:

#import <Foundation/Foundation.h>

int main () {
__block int val = 1;
void (^block)(void) = ^{
val = 9;
};
block();
return 0;
}

使用clang命令对其进行重写之后的主要C++代码如下:

// 结构体,包装了block的函数体定义(即调用block执行的代码块)和暴block的isa指针(即block的类型)
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 结构体,包装了__block修饰的val变量
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_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; // by ref
// 构造函数后面的: val(_val->__forwarding)是初始化列表,初始化列表是C++特有语法,只能使用早构造函数中。: val(_val->__forwarding)的意思是把形参val的__forwarding赋值给成员变量val,相当于val = val->__forwarding;
__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的size等信息
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的函数体定义
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) = 9;
}
// 函数,系统将会在合适的时机将栈上的包装成结构体__Block_byref_val_0的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*/);}

int main() {
// 把val包装成__Block_byref_val_0类型的val。对应于源码中的 __block int val = 1;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
// 调用__main_block_impl_0结构体的构造方法__main_block_impl_0来初始化一个__main_block_impl_0类型的名为block的实例变量
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
// 执行block,对应于源码中的 block();
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

如上,Objective-C源码转换为C++代码之后,主要包括4个结构体(__block_impl、__Block_byref_val_0、__main_block_impl_0、__main_block_desc_0)和3个函数(__main_block_func_0、__main_block_copy_0、__main_block_dispose_0)

结构体__block_impl

__block_impl是一个结构体,包含如下4个成员变量:

  • void *isa;
  • int Flags;
  • int Reserved;
  • void *FuncPtr;

和上一篇文章利用Clang探究block的本质 中的结构体__block_impl一样,isa指针将来指向block的类型。Flags被初始化为0。Reserved是保留的成员变量,暂时没有实际意义。FuncPtr是一个函数指针,指向block的函数体。

结构体__Block_byref_val_0

__Block_byref_val_0也是一个结构体,该结构体包含如下5个成员变量:

  • void *__isa;
  • __Block_byref_val_0 *__forwarding;
  • int __flags;
  • int __size;
  • int val;

第一个成员变量isa指针不用多说。第二个成员变量__forwarding是一个__Block_byref_val_0类型的结构体对象。第三个成员变量__flags,是标志性参数,暂时没用到默认为0。第四个成员变量__size是该结构体所占用的大小。第五个成员变量val无论从名称还是从类型看起来都很熟悉,它就是Objective-C层面的block外部的局部变量val。

结构体__main_block_impl_0

__main_block_impl_0是一个结构体,和利用Clang探究block的本质 中的__main_block_impl_0基本相同,只是多了一个__Block_byref_val_0类型的val,其次构造函数的入参略有不同。
__main_block_impl_0的成员变量val:

- __Block_byref_val_0 *val; // by ref

成员变量val表示包装原始值val的结构体,在构造函数__main_block_impl_0中进行了初始化。

__main_block_impl_0的构造函数:

__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_byref_val_0对impl.isa、impl.Flags、impl.FuncPtry以及Desc和val进行了初始化。其中,构造函数后面的: val(_val->__forwarding)是初始化列表,初始化列表是C++特有语法,初始化列表只能使用在构造函数中。: val(_val->__forwarding)的意思是把形参val的__forwarding赋值给成员变量val,以上构造函数的实现等同于:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
val = val->__forwarding;
}

结构体__main_block_desc_0

__main_block_desc_0是一个结构体。该结构体除了包括reserved和Block_size两个成员变量之外,还包括两个函数copy和dispose。如下:

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);

函数__main_block_func_0

__main_block_func_0是一个函数。函数的实现对应于Objective-C层面的block的定义。如下:

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) = 9;
}

该函数接收一个__main_block_impl_0类型的变量__cself,然后整个函数体相当于执行了如下代码

__cself->val->__forwarding->val = 9;

其中__cself->val是结构体__Block_byref_val_0类型的变量,而__cself->val->__forwarding也是结构体__Block_byref_val_0类型的变量,那么为什么不直接执行__cself->val->val = 9;而是要绕一圈间接的设置__forwarding.val呢?原因下面解释。

函数__main_block_copy_0

__main_block_copy_0是一个函数。其内部调用了_Block_object_assign。将来会被赋值给__main_block_desc_0的成员变量“copy”。其函数定义如下:

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*/);}

结合函数名和函数定义可以得知,该函数的作用是将栈上的包装成结构体__Block_byref_val_0的val拷贝到堆上。当block进行copy操作的时候就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部会调用_Block_object_assign函数。

函数__main_block_dispose_0

__main_block_dispose_0是一个函数。其内部调用了函数_Block_object_dispose。将来会被赋值给__main_block_desc_0的成员变量“dispose”
当block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数做一些销毁操作。

_Block_object_dispose会对val对象做释放操作,类似于release,也就是断开对val对象的引用,而val究竟是否被释放还是取决于val对象本身的retainCount。

main函数重写前后对比

如下是main函数重写前(Objective-C版)和重写后(C++版)的代码,我们对比着来分析。
Objective-C版源码:

int main () {
    __block int val = 1;
    void (^block)(void) = ^{
        val = 9;
    };
    block();
    return 0;
}

Clang重写后C++代码:

int main() {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
void (*block)(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 *)block)->FuncPtr)((__block_impl *)block);
return 0;
}

对比两个main函数,不难发下如下两句代码是等价的:

__block int val = 1;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};

对比以上两行代码,我们发现,编译器将我们定义的val变量包装到了一个__Block_byref_val_0结构体同名变量val中。其中isa指针传递的是(void *)0, __forwarding传递的是结构体变量val自己的地址。Flags传递的是0,__size传递的是__Block_byref_val_0的size,结构体val变量内部的val初始化为1。
所以,__block修饰的局部变量在底层被包装进了一个结构体中。
然后对比block的定义,我们也很容易的发现如下代码是等价的:

void (^block)(void) = ^{
val = 9;
};
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

试图对比以上两段代码,但是发现重写后的C++代码多了太多的类型转换,对其进行精简后,得到如下代码:

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

精简后的代码更加直观易懂,不难看出以上代码等号右侧就是调用了结构体__main_block_impl_0的同名构造函数__main_block_impl_0构造了一个结构体实例赋值给了左侧的block变量。
等等,刚才说这句C++代码等价于Objective-C层面的block定义,即等价于:

void (^block)(void) = ^{
val = 9;
};

那么和Objective-C的block中val = 9;对应的代码去哪了呢?
答案在构造函数的第一个参数中,构造函数的第一个参数是__main_block_func_0,上面我们说过,__main_block_func_0是一个函数,而上篇文章中也说过,C和C++语言中函数名就是函数指针。而这个函数的实现恰是block的函数体定义,如下:

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) = 9;
}

通过查看构造函数的实现代码,我们可以发现,是__main_block_func_0函数指针被赋值给了impl的FuncPtr成员变量。所以将来调用FuncPtr就等价与调用__main_block_func_0函数,也就等价于调用了Objective-C层面的block的函数体实现。

构造函数的第二个参数是__main_block_desc_0结构体实例\__main_block_desc_0_DATA
构造函数的第三个参数是__Block_byref_val_0类型的结构体变量val
构造函数的第四个参数是flags,只是这里被赋值为570425344。

最后对比block的调用,如下分别是Objective-C和C++的调用代码:

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

这里也有一个类型转换block是

对C++代码稍作精简后如下:

(((__block_impl *)block)->FuncPtr)((__block_impl *)block);

我们发现,和利用Clang探究block的本质中一样,block本来是__main_block_impl_0类型的实例,这里却被强制转换为了__block_impl类型并且无论是在编译时还是运行时都不会报错也不会访问非法内存地址。归根到底,还是因为__block_impl是__main_block_impl_0结构体的第一个成员变量,__main_block_impl_0实例block的内存起始地址就是他的__block_impl类型的成员变量impl的内存地址。换句话说,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以转化成功。并且可以合法的访问FunPtr。

对C++代码精简之后如下:

(block->FuncPtr)(block);

显而易见,对block的调用本质上是调用了block这个结构体实例的函数指针FuncPtr,上面我们说过,调用这个函数指针就是调用block的函数体实现代码。
分析下来,不难发现,以上main函数的C++层面的执行过程和利用Clang探究block的本质中所述简直一模一样。
不一样的地方在于__main_block_func_0的函数体实现部分变成了:

__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 9;

把以上代码合二为一之后如下:

__cself->val->__forwarding->val = 9;

其中__cself就是我们调用(block->FuncPtr)(block);时传递的__main_block_impl_0类型的变量block。__cself->val也就是结构体__main_block_impl_0中__Block_byref_val_0类型的成员变量val。__cself->val->__forwarding是结构体__Block_byref_val_0中__Block_byref_val_0类型的成员变量__forwarding,__Block_byref_val_0中除了__forwarding之外还有一个int型的val。
那么接上面的问题:__cself->val是结构体__Block_byref_val_0类型的变量,而__cself->val->__forwarding也是结构体__Block_byref_val_0类型的变量,那么为什么不直接执行__cself->val->val = 9;而是要添加__forwarding这个实例变量,然后间接的通过__forwarding设置int型val的值呢?

原因:添加__forwarding是为了存储变量的内存地址,在对block进行copy之前,__forwarding中存的是自己的内存地址,所以访问val->__forwarding就等于访问val自身;但是对block进行copy,block会从栈区拷贝到堆区,block的__Block_byref_val_0类型的变量也会一并从栈区拷贝到堆区,所以就会有生成一份新的val变量,拷贝之后的val->__forwarding存储的就是堆区中变量的内存地址。
所以,(val->__forwarding->count) = 9;这样赋值,可以保证不管变量是否从栈区拷贝到了堆区,都能正确的访问实际的变量。

结论

__block底层将被修饰的外部变量x包装成了C++结构体。该结构体的实例x和外变量x同名,该实例包含一个结构体指针__forwarding和外部变量x。当我们调用block函数体更新外部变量时,就会调用到x->__forwarding->x = xxx;进而实现外部变量的修改。

补充

__block 不同于 __strong 和 __weak,后面两个是所有权说明符,而__block是存储域说明符,该说明符会影响变量的存储域
常见的存储域说明符还有:

  • typedef
  • extern
  • static
  • auto
  • register

auto表示修饰的变量作为局部变量存储在栈中,这是函数或方法内部默认的声明方式,一般不用添加。auto int num = 0 该语句声明num为一个自动局部变量,意味着进入该模块时,自动为其分配存储空间,退出该模块时自动解除分配。等价于int num = 0
而static表示修饰的变量作为静态变量存储在全局变量区。
static变量有默认的初始值0,而auto变量没有默认的初始值。除非显式的为auto变量赋值,否则它的值是不确定的。这也就是为什么上面在声明num的同时也对num进行了定义。

extern修饰的全局变量,也是存储在全局变量区,虽然都是存储在全局变量区,但是extern与static还是有很大区别的:extern表示修饰的全局变量默认是有外部链接的,作用域是整个工程。在一个文件内定义的全局变量,在另一个文件中,通过extern声明全局变量,就可以使用全局变量;static修饰的全局静态变量,作用域是声明此变量所在的文件。

相关文章

Block原理,为什么block能捕获变量 -- 原理篇
__block 说明符的作用以及其对Clang编译器的影响

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

推荐阅读更多精彩内容