
日常开发中经常会用到 Block,但如果对它的底层实现没有深入地挖掘过,就不能算是真正掌握,本篇就来探究一下 Block 的底层实现原理。
先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。
typedef void(^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 30;
// 创建
MyBlock blk = ^{
NSLog(@"My age is %d .", age);
};
// 执行
blk();
}
return 0;
}
为了探究 Block 的本质,我们需要借助 clang 将含有 Block 语法的源代码转换成 C++ 代码。
终端执行 $ clang -rewrite-objc main.m 命令,就可以将 main.m 文件编译生成 main.cpp 文件,这里截取了 main.cpp 文件中与 block 相关的代码,并添加了部分注释:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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)};
// Block 的结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// block 的 { } 里边的代码构成的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}
// main() 函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
int age = 30;
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
下面开始一步步讨论源码。从 main() 函数开始,关于自动释放池的代码不在此处讨论,先看一下 block 的创建过程:
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 简化后的代码:
MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
我们注意到这里实际上有两个函数:__main_block_impl_0() 和 __main_block_func_0()。
先来看后者,具体代码如下,实际是 block 的 { } 里边的代码构成的函数。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}
然后搜索前一个函数的函数名 __main_block_impl_0,发现它位于下边这个结构体里边:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; // 指明该 block 的类型(此处是栈上的 block)。
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
而这个结构体就是 block 经编译后得到的结构,很明显 __main_block_impl_0() 是它的构造函数,我们发现,这个构造函数里边都是在给 block 的前 3 个元素 (2 个结构体和一个 int age) 赋值。
第一个元素 impl ,它的组成是这样的:
struct __block_impl {
void *isa; // 用于说明 block 的类型
int Flags; // 标识位
int Reserved; // 保留字段
void *FuncPtr; // 指针
};
FuncPtr 是一个指针,根据名字推断应该是一个函数指针,结合 main() 函数中执行构造函数创建 block 的过程可以看出,FuncPtr 指向的是 block 的 { } 里边的代码构成的函数。
isa 指明了block 的类型,构造函数中给它赋的值是 &_NSConcreteStackBlock,说明他是栈上的 block。关于 block 的类型,下一小节就会讲到。
第二个元素 Desc 是 __main_block_desc_0 类型的结构体,如下所示:
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)};
其中 Block_size 从名字推断应该是结构体的大小,紧随其后定义了一个 __main_block_desc_0 类型的变量 __main_block_desc_0_DATA,它的第二个元素值就是当前 block 的大小 sizeof(struct __main_block_impl_0),从 main() 函数中执行 block 构造函数的语句可以看出,__main_block_desc_0_DATA 最终赋值给了 block 中的 Desc,进一步验证了 Block_size 中存放的是 block 的大小。
第三个元素 int age 是 block 捕获的一个 auto 变量,关于捕获变量的机制,后面会详细讨论。
最后回到 main() 函数的最后一行代码:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 简化后:
blk->FuncPtr(blk);
很明显这是执行 blk 里边的指针 FuncPtr 指向的函数,而且将 blk 自己传了进去,这样,就可以在函数内部访问到 block 捕获的变量,如前文提到的 int age。
至此,本文开头的 block 的底层结构基本介绍完了,看起来比较零散,这里绘制了一张总图做个简单小结:

在此,简单说明一下 block 的类型,block 的 3 种类型及其内存分布如下:

那么这 3 种类型的 Block 有什么区别呢,为了搞清楚这个问题,我们需要先回顾一下 4 种常见的变量类型及其代码示例:
// *** 4 中变量的代码示例:
// 全局变量
int global_var = 10;
// 静态全局变量
static int static_global_var = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 自动变量(局部变量)
int local_var = 30; // <==> auto int local_var = 30;
// 静态局部变量
static int local_static_var = 40;
MyBlock blk = ^{
NSLog(@"\n global_var: %d\n static_global_var: %d\n local_var: %d\n local_static_var: %d\n", global_var, static_global_var, local_var, local_static_var);
};
blk();
}
return 0;
}
关于各种 Block 的区别,可以简单汇总成下边的图表:

也就是说:
上文提到了对栈上 Block 的 copy 操作,那么为什么需要 copy 呢?原因是:设置在栈上的 Block 如果其所属的作用域结束,该 Block 就会被废弃,为了延长它的生命周期,就需要将其复制到堆上。
既然栈上的 block 经 copy 后会从栈上复制到堆上,那么另外两种 Block 执行 copy 操作又会发生什么呢? 每一种 Block 被 copy 后的结果如下:

ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如满足一下条件之一时:
MRC 环境下,需要手动调用 block 的 copy 操作,才能将栈上的 block 复制到堆上。
为了保证 Block 内部能够正常访问外部的变量,block有个变量捕获机制,我们以前边介绍常见变量类型的代码为例,看看 Block 是怎么捕获变量的。
执行 clang -rewrite-objc main.m 之后,转换的 block 的源码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 捕获的变量
int local_var;
int *local_static_var;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
从上边的源码可以看出来,对于这 4 种不同的变量,实际只捕获了自动变量 local_var和静态局部标量 local_static_var,而且自动变量是捕获了值,静态局部变量捕获的是变量地址。至于为什么这么设计,推测可能的原因如下:

实际开发中,block 捕获到的变量基本都是自动变量(局部变量),理由是:对于全局变量,任何地方都可以访问它,不安全;对于静态局部变量,它会一直存在于内存中,对内存是一种浪费。
对于基本数据类型的自动变量,前边已经讲过了,就是简单的值捕获,接下来我们重点讨论一下对象类型 auto 变量的捕获。
下边是 block 访问外部对象类型 auto 变量的简单实例。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
MyBlock blk = ^{
NSLog(@"%@", obj);
};
blk();
}
return 0;
}
执行 clang -rewrite-objc main.m 后,生成的源码中有这 2 点不同:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *obj;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
copy 和 dispose,整个文件里新增了 2 个函数 __main_block_copy_0() 和 __main_block_dispose_0(),结合上下问可以知道,这两个函数地址最终传给了 block 里 Desc 中的 copy 和 dispose 这 2 个指针。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
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
这两个函数调用时机和作用如下:
如果 block 从栈上拷贝到堆上
会调用 block 内部的 copy() 函数,此函数内部会调用 _Block_object_assign() 函数,它会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成 强引用 或 弱引用。
如果 block 从堆上移除
会调用 block 内部的 dispose() 函数,此函数内部会调用 _Block_object_dispose() 函数,它会自动释放引用的 auto 变量(即 release)。
另外,如果 block 一直是在栈上,将不会对 auto 变量产生强引用。
前边我们只是在 block 内部使用变量,事实上,如果直接修改变量的话,比如下边这个例子,就会报错:此变量不可赋值 (错误信息见注释)。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MyBlock blk = ^{
age = 20; // Error: Variable is not assignable (missing __block type specifier)
NSLog(@"%d", age);
};
blk();
}
return 0;
}
按照错误信息的提示,如果给 int age = 10; 前边加上 __block,就可以解决 block 内部无法修改 auto 变量的问题,实际操作后,发现果然可以正常输出 age 的新值 20。
现在来看看 __block 修饰符到底做了什么,先将上边的代码转成 C++ 源码,下边截取了其中部分关键代码:
// 新出现的结构体
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}
return 0;
}
先看 main() 函数,__block int age 变成了 __Block_byref_age_0 类型的 age ,也就是说编译器将 __block 修饰的变量包装成了一个新的结构:
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
__Block_byref_age_0 这个结构体里边有一个 int age ,用于存储 age 的值 (10)。还有一个重要成员 __Block_byref_age_0 *__forwarding; ,结合 block 的构造函数,我们知道 __forwarding 指针实际指向了它所在的结构体。

之所以这么做是为了当 block 被拷贝到堆上以后,无论访问栈上的 block 还是 堆上的 block,最终都是访问的堆上的同一个 block(拷贝后,堆上的 __forwarding 指向自己所在的 __block 变量,栈上的 __forwarding 指向堆上的 __block 变量),如下图所示。

接下来,看看 block 的结构 __main_block_impl_0,里边多了一个变量 __Block_byref_age_0 *age;,即 block 捕获了这个新的结构体 __Block_byref_age_0 的地址,所以 block 里边就可以通过地址访问这个结构体,进而修改里边 int age 的值。
__block 修饰的对象类型的 auto 变量与此类似,差别仅在于新生成的结构体:

从上图可知,__block 修饰的对象类型转换后的结构体里边多了两个函数指针,他们分别指向下面 2 个函数,负责内存管理的相关操作,下边就会讲到。
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
对于 被 __block 修饰的对象类型,内存管理分以下 3 种情况:
1.当 __block 变量 在栈上时,不会对指向的对象产生强引用。
2.当 __block 变量 被 copy 到堆时,分两种情况:
__block 变量内部 的 copy 函数,它会调用 _Block_object_assign() 函数,此函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSObject *obj = [[NSObject alloc] init];
MyBlock block = [^{
NSLog(@"%p", obj);
} copy];
block();
[obj release];
}
return 0;
}
如上所示,在 MRC 环境下,对象前加了 __block,不会对 block 形成强引用, 即当执行完 [obj release]; 之后,person 就被释放了。

__block 变量内部 的 dispose() 函数,它会调用 _Block_object_dispose() 函数,此函数会自动释放指向的对象(release)当block在栈上时,对它们都不会产生强引用
当 block 拷贝到堆上时,都会通过 copy 函数来处理它们
对于 __block变量(假设变量名叫做a),最终会执行 _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对于对象类型的 auto 变量(假设变量名叫做p),最终会执行 _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
以上两者最终调用的方法是相同的,只不过最后一个参数有差别,前者是 8(表示引用类型),后者是 3 (表示对象),下面对 _Block_object_dispose() 函数的调用与之类似。
当 block 从堆上移除时,都会通过 dispose 函数来释放它们
对于 __block变量(假设变量名叫做a),最终会执行 _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对于对象类型的auto变量(假设变量名叫做p),最终会在执行 _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
关于使用 block 可能遇到的循环引用问题,我们分 ARC 和 MRC 两种情况进行讨论。
在 ARC 环境下,目前大概有 3 种常见的解决循环引用的方式:
此方式是最常见也是推荐使用的一种方式,基本原理是,self 对 block 的引用维持强引用,不过将 block 对 self 的引用改成了弱引用。
__weak typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
这种方式与上边的方式类似,不过当 weakSelf 指向的对象销毁后,指针已然指向那块已经被回收的内存,可能发生野指针错误,所以是不安全的。
__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
__block typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
weakSelf = nil;
};
self.block();
我们知道,当在变量前边加了 __block 之后就多了一个 __blcok 变量,于是里边的引用关系就变成了:

为了打破这个循环引用的关系,需要在 block 里边将对象置为 nil,而且必须执行 block 才能断开 __block 变量对对象的强引用。

MRC 环境下解决循环引用的方式与 ARC 环境类似,只是由于 MRC 环境下不可以使用 weak,所以只有 __unsafe_unreturned 和 __block 2 种解决方式。对于 __block 的方式,在MRC中,__block 变量 不会对 weakSelf 产生强引用,也就不需要将其置为 nil 并执行 block 了。
__block typeof(self) weakSelf = self;
self.block = ^{
printf("%p", weakSelf);
};
以上就是对 Block 底层实现的一个简单讨论,受自己的知识积累所限,难免有理解不到位的地方,后期会及时修正。
我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere
我在理解Enumerator.new方法的工作原理时遇到了一些困难。假设文档中的示例:fib=Enumerator.newdo|y|a=b=1loopdoy[1,1,2,3,5,8,13,21,34,55]循环中断条件在哪里,它如何知道循环应该迭代多少次(因为它没有任何明确的中断条件并且看起来像无限循环)? 最佳答案 Enumerator使用Fibers在内部。您的示例等效于:require'fiber'fiber=Fiber.newdoa=b=1loopdoFiber.yieldaa,b=b,a+bendend10.times.m
我没有理解以下行为(另请参阅inthisSOthread):defdef_testputs'def_test.in'yieldifblock_given?puts'def_test.out'enddef_testdoputs'def_testok'endblock_test=procdo|&block|puts'block_test.in'block.callifblockputs'block_test.out'endblock_test.calldoputs'block_test'endproc_test=procdoputs'proc_test.in'yieldifblock_gi
我需要尝试一些AES片段。我有一些密文c和一个keyk。密文已使用AES-CBC加密,并在前面加上IV。不存在填充,纯文本的长度是16的倍数。所以我这样做:aes=OpenSSL::Cipher::Cipher.new("AES-128-CCB")aes.decryptaes.key=kaes.iv=c[0..15]aes.update(c[16..63])+aes.final它工作得很好。现在我需要手动执行CBC模式,所以我需要单个block的“普通”AES解密。我正在尝试这个:aes=OpenSSL::Cipher::Cipher.new("AES-128-ECB")aes.dec
最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总
Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图
我正在尝试为我的iOS应用程序设置cocoapods但是当我执行命令时:sudogemupdate--system我收到错误消息:当前已安装最新版本。中止。当我进入cocoapods的下一步时:sudogeminstallcocoapods我在MacOS10.8.5上遇到错误:ERROR:Errorinstallingcocoapods:cocoapods-trunkrequiresRubyversion>=2.0.0.我在MacOS10.9.4上尝试了同样的操作,但出现错误:ERROR:Couldnotfindavalidgem'cocoapods'(>=0),hereiswhy:U
我在使用自定义RailsFormBuilder时遇到了问题,从昨天晚上开始我就发疯了。基本上我想对我的构建器方法之一有一个可选block,以便我可以在我的主要content_tag中显示其他内容。:defform_field(method,&block)content_tag(:div,class:'field')doconcatlabel(method,"Label#{method}")concattext_field(method)capture(&block)ifblock_given?endend当我在我的一个Slim模板中调用该方法时,如下所示:=f.form_field:e
我从用户Hirolau那里找到了这段代码:defsum_to_n?(a,n)a.combination(2).find{|x,y|x+y==n}enda=[1,2,3,4,5]sum_to_n?(a,9)#=>[4,5]sum_to_n?(a,11)#=>nil我如何知道何时可以将两个参数发送到预定义方法(如find)?我不清楚,因为有时它不起作用。这是重新定义的东西吗? 最佳答案 如果您查看Enumerable#find的文档,您会发现它只接受一个block参数。您可以将它发送两次的原因是因为Ruby可以方便地让您根据它的“并行赋
我明白了defa(&block)block.call(self)end和defa()yieldselfend导致相同的结果,如果我假设有这样一个blocka{}。我的问题是-因为我偶然发现了一些这样的代码,它是否有任何区别或者是否有任何优势(如果我不使用变量/引用block):defa(&block)yieldselfend这是一个我不理解&block用法的具体案例:defrule(code,name,&block)@rules=[]if@rules.nil?@rules 最佳答案 我能想到的唯一优点就是自省(introspecti