本篇学习内容
block的本质
block的原理是怎样的?本质是什么?
- block本质上也是一个OC对象,因为它的内部也有个isa指针
- block是封装了函数调用以及函数调用环境的OC对象
通俗的理解:block就是将一些代码封装起来,以便在将来某个时候被使用,如果你不去调用block,block内部封装的代码就不会执行。
举一个简单的例子
int main(int argc, const char * argv[]) {
@autoreleasepool {
^{
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
};
}
return 0;
}
RUN>
没有任何输出,Block代码块没调用
Block的使用也很简单,可以像函数一样被使用。加上()
就代表调用,如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
^{
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
NSLog(@"this is a block!");
}();
}
return 0;
}
RUN>
2021-04-22 15:35:54.161606+0800 Interview03-block[4322:148955] this is a block! 2021-04-22 15:35:54.162084+0800 Interview03-block[4322:148955] this is a block! 2021-04-22 15:35:54.162138+0800 Interview03-block[4322:148955] this is a block! 2021-04-22 15:35:54.162160+0800 Interview03-block[4322:148955] this is a block!
block的底层结构-block的本质探索
写个简单的block,其中block内部使用了block外部的age变量:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
};
block(10, 10);
}
return 0;
}
RUN>
2021-04-22 15:38:10.449976+0800 Interview03-block[4340:150550] this is a block! -- 20
上面的代码可以看出,block里面使用了它上面的
int age = 20
,可以将这个先简单的理解成函数调用环境,顾名思义,就是block所用到的一些外部变量。
通过clang编译器执行编译成C++代码:
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
clang编译器编译完后会得到一个.cpp格式的文件,这就是我们刚才转换的.m文件的底层代码.
main函数的C++代码如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 20;
//block底层定义
void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
//block底层调用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
}
return 0;
}
但是由于底层的代码添加了许多强转,我们简化代码,如下:
//block底层定义
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
//block底层调用
block->FuncPtr(block, 10, 10);
block底层定义
//block底层定义
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
先看__main_block_impl_0这个函数,我们发现它被定义在一个同名结构体里面,这个__main_block_impl_0结构体就是block的底层实现
// 一: block底层数据结构
struct __main_block_impl_0 {
struct __block_impl impl; // 1: impl 结构体
struct __main_block_desc_0* Desc; // 2: block描述信息的结构体
int age; //3:捕获的外部变量
//4: 和结构体同名的构造函数 ( C++语法 , 类似于 OC 的init方法,返回一个结构体对象,类似于返回self)
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age){
impl.isa = &_NSConcreteStackBlock;//isa指向类对象,就比如str的isa指向NSString
impl.Flags = flags;
impl.FuncPtr = fp;//外面的__main_block_func_0函数地址传进来,保存在这里
Desc = desc;//外面的__main_block_desc_0结构体地址传进来,保存在这里
}
};
通过底层代码我们可以看到,block在底层中的数据结构是一个结构体,这个结构体有四个部分组成:
1: struct __block_impl
2: struct __main_block_desc_0
3: 捕获的外部变量
4:和block结构体同名的构造函数
我们找到struct __block_impl 结构体
:
//struct __block_impl 结构体
struct __block_impl {
void *isa; //指向 block 的类型
int Flags;//按位表示block的附加信息
int Reserved;//保留变量
void *FuncPtr; //封装了执行 block 代码块的函数地址
};
发现这个结构体里面第一个成员就是isa,验证了block本质上也是一个OC对象。
然后我们再找到struct __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)};
可以发现,这个结构体被重新命名为__main_block_desc_0_DATA,默认传入了两个值0和sizeof(struct __main_block_impl_0),block的底层就是__main_block_impl_0结构体,所以这个结构体==第二个值保存的是block的大小==。
接下来我们看一下__main_block_impl_0函数的参数,第一个参数是指向__main_block_func_0函数的指针,如下:
//封装了block执行逻辑的函数
//第一个参数是block,后面是block调用的时候传入的参数
void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
}
现在我们知道了,首先__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age)
函数有两个参数,第一个参数是__main_block_func_0函数的地址(这个函数里面封装了我们block里面执行的代码),第二个参数是__main_block_desc_0结构体的地址(这个结构体里面有保存block的大小),构造函数的返回值是个__main_block_impl_0结构体,block底层就是__main_block_impl_0结构体,最后再获取__main_block_impl_0结构体的地址,赋值给左边的“block”变量,然后我们拿到“block”变量就可以做其他事情了,至此,block定义完成。
block底层调用
//block底层调用
block->FuncPtr(block, 10, 10);
这句代码就很简单了,直接取出block里面的FuncPtr函数,传入参数进行调用。
不应该是通过“block-> impl->FuncPtr(block, 10, 10)”来拿到FuncPtr吗?其实我们在简化之前,代码是这样的:
((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
可以发现系统把block强转成__block_impl类型的了,由于impl又是__main_block_impl_0结构体的第一个成员,所以impl的地址和__main_block_impl_0结构体的地址是一样的,强转之后可以直接获取到FuncPtr。
根据如上分析,验证了,block是封装了函数调用以及函数调用环境的OC对象。
__main_block_impl_0``__block_impl
与__main_block_desc_0
3个结构体之间的关系
咋们通过梳理了block底层几个类的关系,现在调整一下代码
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
};
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;
int age;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
};
struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
block(10, 10);
}
return 0;
}
加断点,验证一下内存
FuncPtr地址验证
总结:
如上图所示,block底层就是一个__main_block_impl_0结构体,它由三个部分组成:
- 第一部分是impl,它是个结构体,里面有isa指针和FuncPtr指针,FuncPtr指针指向__main_block_func_0函数,这个函数里面封装了block需要执行的代码。
- 第二部分是desc,它是个指针,指向__main_block_desc_0结构体,它里面有一个Block_size用来保存block的大小。
- 第三部分是age,它把外面访问的成员变量age封装到自己里面了。
block的变量捕获(capture)
- 如果是被auto修饰的局部变量,会被捕获,是值传递
- 如果是被static修饰的局部变量,会被捕获,是指针传递
- 如果是全局变量,不会被捕获,因为可以直接访问
一:auto变量
- auto变量:自动变量,离开作用域就会销毁,一般我们创建的局部变量都是auto变量,比如
int age = 10
,系统会在默认在前面加上auto int age = 10
首先我们要搞清楚,什么是捕获,所谓捕获外部变量,意思就是在block内部,创建一个变量来存放外部变量,这就叫做捕获.
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 20;
block();
}
return 0;
}
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
{
int age = 10;
//定义block
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age
);
age = 20;
//调用block
block->FuncPtr(block);
return 0;
}
我们看到在调用block的构造函数时,传入了三个参数,分别是:__main_block_func_0
,&__main_block_desc_0_DATA
,age
,我们找到block
的构造函数,看看内部如何处理这个age
:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 1: 定义了一个同名的age变量
//block构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
age = _age; //2 :C++的特殊语法,在构造函数内部会默认把_age赋值给age
}
};
通过查看block
的内部结构看我们发现,block
内部创建了一个age
变量,并且在block
构造函数中,把传递进来的_age
赋值给了这个age
变量.我们看看调用block
时,他的底部取的是哪个age
:
//调用block的FuncPtr函数,把block当做参数传递进去
block->FuncPtr(block);
//FuncPtr函数内部
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//通过传递的block,找到block内部的age
int age = __cself->age; // bound by copy
//打印age
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_3089d7_mi_0,age);
}
通过底层代码,我们看到,在调用block
时,block
会找到自己内部的age
变量,然后打印数出,所以我们修改age = 20
,并不会影响block
内部的age
值
二:static变量
我们执行如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10;
static int height = 10;
void (^block)(void) = ^{
// age的值捕获进来
// height的指针捕获进来
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
RUN>
2021-04-22 16:31:33.658732+0800 Interview01-Block的本质[4670:177646] age is 10, height is 20
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
代码转成C++代码,抽取关键的代码,如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_12eb7d_mi_1, age, (*height));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int height = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));//&height传递指针
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看出age和height都被捕获了,age是值捕获,height是指针捕获。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;//定义 age 变量
int *height;//定义一个 指针变量,存放外部变量的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
定义了两个变量age
,height
,不同的是,height
是一个指针指针变量
,用于存放外部变量的指针.我们再来看看执行block
代码块的内部:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
// *height : 取出指针变量所指向的内存的值
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_bf6cae_mi_0,age,(*height));
}
我们看到,对于age
是捕获到内部,把外部age
的值存起来,而对于height
,是把外部变量的指针保存起来,所以,我们在修改height
时,会影响到block
内部的值
- 我们定义block,其实就是初始化__main_block_impl_0结构体,定义block的时候会把age和&height传进去,这个结构体里面有一个age一个height指针用于接收传进去的值,这行代码“age(_age), height(_height)”就是保证外面的变量改变的时候实时改变结构体里面的age和height指针的值。
- 我们调用block的时候,其实就是执行__main_block_func_0函数,这个函数会获取__main_block_impl_0结构体中age和height指针的值,所以打印的时候就会把age和*height的值打印出来。
- 所以执行完block之后age的值没改变,因为是值传递,height的值改变了,因为是指针传递。
为什么auto变量是值传递,static变量是指针传递呢?
因为auto
是自动变量,出了作用域后会自动销毁的,如果我们保留他的指针,就会存在访问野指针的情况
//定义block类型
void(^block)(void);
void test(){
int age = 10;
static int height = 20;
//在block内部访问 age , height
block = ^{
NSLog(@"age is %d, height is %d",age,height);
};
age = 20;
height = 20;
}
//在main函数中调用
int main(int argc, const char * argv[]) {
test();
//test调用后,age变量就会自动销毁,如果block内部是保留age变量的指针,那么我们在调用block()时,就出现访问野指针
block();
}
三:全局变量
全局变量哪里都可以访问,所以block
内部是不会捕获全局变量的,直接访问
int age_ = 10;
static int height_ = 10;
void (^block)(void);
void test()
{
block = ^{
// age的值捕获进来(capture)
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
转成C++文件之后,代码如下:
int age_ = 10;
static int height_ = 10;
void (*block)(void);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_yh_qjzhl57s63j2m9l4frv27zjc0000gn_T_main_478a1b_mi_0, age, height);
}
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
void test()
{
block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA));
age = 20;
height = 20;
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
test();
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
为什么全局变量不需要捕获?
因为全局变量无论哪个函数都可以访问,
block
内部当然也可以正常访问,所以根本无需捕获
为什么局部变量就需要捕获呢?
因为作用域的问题,我们在一个函数中定义变量,在
block
内部访问,本质上跨函数访问,所以需要捕获起来.
四:self的捕获
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)test;
- (instancetype)initWithName:(NSString *)name;
@end
#import "MJPerson.h"
@implementation MJPerson
- (void)test
{
void (^block)(void) = ^{
NSLog(@"-------%d", [self name]);
};
block();
}
- (instancetype)initWithName:(NSString *)name
{
if (self = [super init]) {
self.name = name;
}
return self;
}
@end
将MJPerson.m
转成C++代码:
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *self;
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MJPerson__test_block_func_0(struct __MJPerson__test_block_impl_0 *__cself) {
MJPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_MJPerson_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
}
static void _I_MJPerson_test(MJPerson * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0((void *)__MJPerson__test_block_func_0, &__MJPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
static instancetype _I_MJPerson_initWithName_(MJPerson * self, SEL _cmd, NSString *name) {
if (self = ((MJPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJPerson"))}, sel_registerName("init"))) {
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
}
return self;
}
可以看出self被捕获了,并且是指针捕获,既然被捕获,就说明self是局部变量。
为什么self是局部变量呢?
其实每个方法都两个隐式参数,一个是self一个是_cmd,self是方法调用者,_cmd是方法名,既然self被当做参数了,那self肯定是局部变量了,也可以在上面的代码中进行验证,我们看一下转换后的test()
方法:
static void _I_Person_test(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
OC 中的test()
方法时没有参数的,但是转换成 C++ 后就多了两个参数self
,_cmd
,其实我们每个 OC 方法都会默认有这两个参数,这也是为什么我们在每个方法中都能访问self
和_cmd
,而参数就是局部变量,所以block
就自然而然的捕获了self
.
对于[self name],在上面的代码可以看出是给self发送消息,如下:
objc_msgSend((id)self, sel_registerName("name"))
所以,block会捕获self,如果想要访问self中的成员变量就给self发送消息就好了(self都被捕获了,肯定可以获取到self中的其他信息了)。
总结:
一:只要是局部变量,不管是auto 变量
,还是static 变量
,block
都会捕获.不同的是,对于auto 变量
,block
是保存值,而static 变量
是保存的指针.
二:如果是全局变量,根本不需要捕获,直接访问。
特别备注
本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!