探索 Block 的本质
定义
- Block 是 C 语言的扩充功能
- Block 是带有自动变量(局部变量)的匿名函数
本质
- Block 是一个 Objc 对象
底层实现
下面我将通过一个简单的例子,结合源代码进行介绍
int main(int argc, const char * argv[]) {void (^blk)(void) = ^{ printf("Hello Block\n"); };blk();return 0;
}
复制代码
使用clang -rewrite-objc main.m
,我们可以将 Objc 的源码转成 Cpp 的相关源码:
int main(int argc, const char * argv[]) {// Block 的创建void (*blk)(void) =(void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);// Block 的使用((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);return 0;
}
复制代码
由上面的源码,我们能猜想到:
- Block 的创建涉及
__main_block_impl_0
结构体 - Block 的涉及到了
FuncPtr
函数指针的调用
从这里为切入点看看上面提到的都是啥
Block 的数据结构
Block 的真身:
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;// 省略了构造函数
};
复制代码
- Block 其实不是一个匿名函数,他是一个结构体
__main_block_impl_0
名字的命名规则:__所在函数_block_impl_序号
impl 变量的数据结构
__main_block_impl_0
的主要数据:
struct __block_impl {void *isa;int Flags;int Reserved;void *FuncPtr;
};
复制代码
isa
指针: 体现了 Block 是 Objc 对象的本质。FuncPtr
指针: 其实就是一个函数指针,指向所谓的匿名函数。
Desc 变量的数据结构
__main_block_desc_0
中放着 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)
};
复制代码
"匿名函数"
__main_block_impl_0
即 Block 创建时候使用到了__main_block_func_0
正是下面的函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Hello Block\n");
}
复制代码
- 这部分和
^{ printf("Hello Block\n"); }
十分相似,由此可看出: 通过 Blocks 使用的匿名函数实际上被作为简单的 C 语言函数来处理 - 函数名是根据 Block 语法所属的函数名(此处
main
)和该 Block 语法在函数出现的顺序值(此处为 0)来命名的。 - 函数的参数
__cself
相当于 C++ 实例方法中指向实例自身的变量this
,或是 Objective-C 实例方法中指向对象自身的变量self
,即参数__cself
为指向 Block 的变量。 - 上面的
(*blk->impl.FuncPtr)(blk);
中的blk
就是__cself
介绍了基本的数据结构,下面到回到一开始的main
函数,看看 Block 具体的使用
Block 的创建
void (*blk)(void) =(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;
*/
复制代码
void (^blk)(void)
就是是一个struct __main_block_impl_0 *blk
- Block 表达式的其实就是通过所谓的匿名函数
__main_block_func_0
的函数指针创建一个__main_block_impl_0
结构体,我们用的时候是拿到了这个结构体的指针。
Block 的使用
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/** 去掉转换的部分(*blk->impl.FuncPtr)(blk);
*/
复制代码
- Block 真正的使用方法就是使用
__main_block_impl_0
中的函数指针FuncPtr
(blk)
这里是传入自己,就是给_cself
传参
Block 的类型
从 Block 中的简单实现中,我们从
isa
中发现 Block 的本质是 Objc 对象,是对象就有不同类型的类。因此,Block 当然有不同的类型
在 Apple 的libclosure-73
中的data.c
上可见,isa
可指向:
void * _NSConcreteStackBlock[32] = { 0 }; // 栈上创建的block
void * _NSConcreteMallocBlock[32] = { 0 }; // 堆上创建的block
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 }; // 作为全局变量的block
void * _NSConcreteWeakBlockVariable[32] = { 0 };
复制代码
其中我们最常见的是:
Block的类型 | 名称 | 行为 | 存储位置 |
---|---|---|---|
_NSConcreteStackBlock | 栈Block | 捕获了局部变量 | 栈 |
_NSConcreteMallocBlock | 堆Block | 对栈Block调用copy所得 | 堆 |
_NSConcreteGlobalBlock | 全局Block | 定义在全局变量中 | 常量区(数据段) |
PS: 内存五大区:栈、堆、静态区(BSS 段)、常量区(数据段)、代码段
关于 copy 操作
对象有copy
操作,Block 也有copy
操作。不同类型的 Block 调用copy
操作,也会产生不同的复制效果:
Block的类型 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 常量区(数据段) | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
栈上的 Block 复制到堆上的时机
- 调用 Block 的
copy
实例方法
编译器自动调用_Block_copy
函数情况
- Block 作为函数返回值返时
- 将 Block 赋值给 __strong 指针(
id
或 Block 类型成员变量) - 在 Apple 的 Cocoa、GCD 等 api 中传递 Block 时
PS: 在 ARC 环境下,声明的 Block 属性用copy
或strong
修饰的效果是一样的,但在 MRC 环境下用 copy 修饰。
捕获变量
基础类型变量
以全局变量、静态全局变量、局部变量、静态局部变量为例:
int global_val = 1;
static int static_global_val = 2;int main(int argc, const char * argv[]) {int val = 3;static int static_val = 4;void (^blk)(void) = ^{printf("global_val is %d\n", global_val);printf("static_global_val is %d\n", static_global_val);printf("val is %d\n", val);printf("static_val is %d\n", static_val);};blk();return 0;
}
复制代码
转换后“匿名函数”对应的代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int val = __cself->val; // bound by copyint *static_val = __cself->static_val; // bound by copyprintf("global_val is %d\n", global_val);printf("static_global_val is %d\n", static_global_val);printf("val is %d\n", val);printf("static_val is %d\n", (*static_val));
}
复制代码
- 全局变量、静态全局变量: 作用域为全局,因此在 Block 中是直接访问的。
- 局部变量: 生成的
__main_block_impl_0
中存在val
实例,因此对于局部变量,Block 只是单纯的复制创建时候局部变量的瞬时值,我们可以使用值,但不能修改值。
struct __main_block_impl_0 {// ...int val; // 值传递// ...
};
复制代码
- 静态局部变量: 生成的
__main_block_impl_0
中存在static_val
指针,因此 Block 是在创建的时候获取静态局部变量的指针值。
struct __main_block_impl_0 {// ...int *static_val; // 指针传递// ...
};
复制代码
对象类型变量
模仿基础类型变量,实例化四个不一样的SCPeople
变量:
int main(int argc, const char * argv[]) {// 省略初始化[globalPeople introduce];[staticGlobalPeople introduce];[people introduce];[staticPeople introduce];return 0;
}
复制代码
转换后"匿名函数"对应的代码:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {SCPeople *people = __cself->people; // bound by copySCPeople **staticPeople = __cself->staticPeople; // bound by copy// 省略 objc_msgSend 转换[globalPeople introduce];[staticGlobalPeople introduce];[people introduce];[*staticPeople introduce];
}
复制代码
- 全局对象、静态全局对象: 作用域依然是全局,因此在 Block 中是直接访问的。
- 局部对象: 生成的
__main_block_impl_0
中存在people
指针实例,因此 Block 获取的是指针瞬间值,我们可以在 Block 中通过指针可以操作对象,但是不能改变指针的值。
struct __main_block_impl_0 {// ...SCPeople *people;// ...
};
复制代码
- 静态局部对象: 生成的
__main_block_impl_0
中存在staticPeople
指针的指针,因此 Block 是在创建的时候获取静态局部对象的指针值(即指针的指针)。
struct __main_block_impl_0 {// ...SCPeople **staticPeople;// ...
};
复制代码
小结
通过对基础类型、对象类型与四种不同的变量进行排列组合的小 Demo,不难得出下面的规则:
变量类型 | 是否捕获到 Block 内部 | 访问方式 |
---|---|---|
全局变量 | 否 | 直接访问 |
静态全局变量 | 否 | 直接访问 |
局部变量 | 是 | 值访问 |
静态局部变量 | 是 | 指针访问 |
PS:
- 基础类型和对象指针类型其实是一样的,只不过指针的指针看起来比较绕而已。
- 全局变量与静态全局变量的存储方式、生命周期是相同的。但是作用域不同,全局变量在所有文件中都可以访问到,而静态全局变量只能在其申明的文件中才能访问到。
变量修改
上面的篇幅通过底层实现,向大家介绍了 Block 这个所谓"匿名函数"是如何捕获变量的,但是一些时候我们需要修改 Block 中捕获的变量:
修改全局变量或静态全局变量
全局变量与静态全局变量的作用域都是全局的,自然在 Block 内外的变量操作都是一样的。
修改静态局部变量
在上面变量捕获的章节中,我们得知 Block 捕获的是静态局部变量的指针值,因此我们可以在 Block 内部改变静态局部变量的值(底层是通过指针来进行操作的)。
修改局部变量
使用
__block
修饰符来指定我们想改变的局部变量,达到在 Block 中修改的需要。
我们用同样的方式,通过底层实现认识一下__block
,举一个?:
__block int val = 0;
void (^blk)(void) = ^{ val = 1; };
blk();
复制代码
经过转换的代码中出现了和单纯捕获局部变量不同的代码:
__Block_byref_val_0
结构体
struct __Block_byref_val_0 {void *__isa; // 一个 Objc 对象的体现__Block_byref_val_0 *__forwarding; // 指向该实例自身的指针int __flags;int __size;int val; // 原局部变量
};
复制代码
- 编译器会将
__block
修饰的变量包装成一个 Objc 对象。
val
转换成__Block_byref_val_0
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val,0,sizeof(__Block_byref_val_0),0
};
复制代码
__main_block_impl_0
捕获的变量
struct __main_block_impl_0 {// ...__Block_byref_val_0 *val; // by ref// ...
};
复制代码
- Block的
__main_block_impl_0
结构体实例持有指向__block
变量的__Block_byref_val_0
结构体实例的指针。 - 这个捕获方式和捕获静态局部变量相似,都是指针传递
"匿名函数"的操作
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) = 1;
}
复制代码
(val->__forwarding->val) 解释
- 左边的
val
是__main_block_impl_0
中的val
,这个val
通过__block int val
的地址初始化 - 右边的
val
是__Block_byref_val_0
中的val
,正是__block int val
的val
__forwarding
在这里只是单纯指向了自己而已
__forwarding 的存在意义
上面的"栈Blcok"中__forwarding
在这里只是单纯指向自己,但是在当"栈Blcok"复制变成"堆Block"后,__forwarding
就有他的存在意义了:
PS:__block
修饰符不能用于修饰全局变量、静态变量。
内存管理
Block 与对象类型
copy & dispose
众所周知,对象其实也是使用一个指针指向对象的存储空间,我们的对象值其实也是指针值。虽然是看似对象类型的捕获与基础类型的指针类型捕获差不多,但是捕获对象的转换代码比基础指针类型的转换代码要多。(__block
变量也会变成一个对象,因此下面的内容也适用于__block
修饰局部变量的情况)。多出来的部分是与内存管理相关的copy
函数与dispose
函数:
底层实现
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->people, (void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->staticPeople, (void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/);
}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
复制代码
这两个函数在 Block 数据结构存在于Desc
变量中:
static struct __main_block_desc_0 {// ...void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);void (*dispose)(struct __main_block_impl_0*);
}; // 省略了初始化好的结构体
复制代码
函数调用时机
函数 | 调用时机 |
---|---|
copy 函数 | 栈上的 Block 复制到堆时 |
dispose 函数 | 堆上的 Block 被废弃时 |
函数意义
copy
函数中的_Block_object_assign
函数相当于内存管理中的retain
函数,将对象赋值在对象类型的结构体成员变量中。dispose
函数中的_Block_object_dispose
函数相当于内存管理中的release
函数,释放赋值在对象类型的结构体变量中的对象。- 通过
copy
和dispose
并配合 Objc 运行时库对其的调用可以实现内存管理
※ 例子
当 Block 内部访问了对象类型的局部变量时:
- 当 Block 存储在栈上时: Block 不会对局部变量产生强引用。
- 当 Block 被
copy
到堆上时: Block 会调用内部的copy
函数,copy
函数内部会调用_Block_object_assign
函数,_Block_object_assign
函数会根据局部变量的修饰符(__strong
、__weak
、__unsafe_unretained
)作出相应的内存管理操作。(注意: 多个 Block 对同一个对象进行强引用的时,堆上只会存在一个该对象) - 当 Block 从堆上被移除时: Block 会调用内部的
dispose
函数,dispose
函数内部会调用_Block_object_dispose
函数,_Block_object_dispose
函数会自动release
引用的局部变量。(注意: 直到被引用的对象的引用计数为 0,这个堆上的该对象才会真正释放)
PS: 对于__block
变量,Block 永远都是对__Block_byref_局部变量名_0
进行强引用。如果__block
修饰符背后还有其他修饰符,那么这些修饰符是用于修饰__Block_byref_局部变量名_0
中的局部变量
的。
现象: Block 中使用的赋值给附有
__strong
修饰符的局部变量的对象和复制到堆上的__block
变量由于被堆的 Block 所持有,因而可超出其变量作用域而存在。
循环引用
由于 Block 内部能强引用捕获的对象,因此当该 Block 被对象强引用的时候就是注意以下的引用循环问题了:
ARC 环境下解决方案
弱引用持有:使用
__weak
或__unsafe_unretained
捕获对象解决weak
修饰的指针变量,在指向的内存地址销毁后,会在 Runtime 的机制下,自动置为nil
。_unsafe_unretained
不会置为nil
,容易出现悬垂指针,发生崩溃。但是_unsafe_unretained
比__weak
效率高。
使用
__block
变量:使用__block
修饰对象,在 block 内部用完该对象后,将__block
变量置为nil
即可。虽然能控制对象的持有期间,并且能将其他对象赋值在__block
变量中,但是必须执行该 block。(意味着这个对象的生命周期完全归我们控制)
MRC 环境下解决方案
- 弱引用持有:使用
__unsafe_unretained
捕获对象 - 直接使用
__block
修饰对象,无需手动将对象置为nil
,因为底层_Block_object_assign
函数在 MRC 环境下对 block 内部的对象不会进行retain
操作。
MRC 下的 Block
ARC 无效时,需要手动将 Block 从栈复制到堆,也需要手动释放 Block
- 对于栈上的 Block 调用
retain
实例方法是不起作用的 - 对于栈上的 Block 需要调用一次
copy
实例方式(引用计数+1),将其配置在堆上,才可继续使用retain
实例方法 - 需要减少引用的时候,只需调用
release
实例方法即可。 - 对于在 C 语言中使用 Block,需要使用
Block_copy
和Block_release
代替copy
和release
。
转载于:https://juejin.im/post/5c6dfd3d6fb9a049dd80e215
探索 Block 的本质相关推荐
- 浅谈OC中Block的本质
Block简介 block是将函数及其执行上下文封装起来的一个对象 在block实现的内部,有很多变量,因为block也是一个对象 其中包含了诸如isa指针,imp指针等对象变量,还有储存其截获变量的 ...
- oc中block的本质及底层原理
block的本质 block的种类及储存区域 __block的本质 block的循环引用 前言: 这里就不讨论block的具体写法及使用场景了,因为当你有一天想深入了解block 的底层原理时,你早已 ...
- 《海底捞你学不会》探索管理的本质
海底捞的管理 我愿意努力工作,因为我盼望明天会更好: 我愿意尊重每一位同事,因为我也需要大家的关心: 我愿意真诚,因为我需要问心无愧: 我愿意虚心接受意见,因为我们太需要成功: 我坚信,只要付出总有回 ...
- 探索编程语言的本质:了解编程语言的定义与分类
前言: 由于我看了一眼我的粉丝列表,发现好像关于开发语言的童鞋占比较多哈,所以出一下这篇专栏. 要关注的小伙伴可以提前订阅哈. 目录 前言: 引言 1.1. 编程语言的重要性 1.2. 本文的目的与结 ...
- Cpp 对象模型探索 / 多态的本质
普通成员函数的调用方式是直接通过编译期间确定的函数地址来调用. 多态是通过查询对象的虚函数表来获取虚函数的地址.因为像工厂模式这样,并不能在编译期间知道基类指向的是哪个子类,也就导致了不能在编译期间获 ...
- 关于block的本质,你懂了吗?
✅作者简介:大家好我是瓜子三百克,一个非科班出身的技术程序员,还是喜欢在学习和开发中记录笔记的博主小白!
- Block背后的数据结构及变量截取
本文的内容主要是基于Clang编译器的官方文档所写. 在开始探索Block的本质之前,大家先试着分析一下,下面的代码会输出什么: void main() {__block int a = 13;int ...
- 向大脑学习智能本质,探索通用 AI 的另一条可行路径
[观点速递]"大数据"."大算力"和"大模型",是近些年人工智能领域的热点词汇.在本届智源大会上发布的超大规模人工智能模型-&q ...
- iOS进阶之底层原理-block本质、block的签名、__block、如何避免循环引用
面试的时候,经常会问到block,学完本篇文章,搞通底层block的实现,那么都不是问题了. block的源码是在libclosure中. 我们带着问题来解析源码: blcok的本质是什么 block ...
最新文章
- 第一学期网络技术知识总汇
- numpy 矩阵计算例子
- java setcharat,Java StringBuffer setCharAt()方法
- NOD32升级ID自动填写工具+更新版1.754
- vlan为什么能隔离广播域_路由交换技术-VLAN原理及配置
- opera价格设置(一)
- JS代码简单一段即可破解QQ空间删除说说
- 如何学习平面设计色彩搭配原理
- 环境化学试题及答案大全
- Exchange 2019反垃圾邮件组件启用反垃圾邮件功能、设置白名单\黑名单
- 一文搞懂大比例尺地形图测绘
- 前端--CSS选择器,盒子模型学习
- java透视图_Eclipse透视图
- Python——数字金字塔
- 机械转行程序员怎么样?
- Java入门-机票购买、座舱等级、淡旺季计算价格
- 【华为内部狂转的想象力惊人的好文】趣谈大数据
- CentOs解决下载速度慢 更换下载源
- MATLAB遗传算法工具箱Genetic Algorithm Toolbox的下载和安装
- 计算机硬件系统 — 冯诺依曼体系结构运行原理解析