目录
Block的本质
针对以下代码,我们来窥探下block的本质
int main(int argc, char * argv[]) {
@autoreleasepool {
void(^blk)(void)= ^(void){
printf("Block\n");
};
blk();
}
return 0;
}
执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 并精简代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
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)};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
int main(int argc, char * argv[]) {
void(*blk)(void)= &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA));
((__block_impl *)blk)->FuncPtr(blk);
}
return 0;
}
从以上源码分析:
变量blk是指向结构体__main_block_impl_0的指针。
1.__main_block_impl_0结构体的FuncPtr是指向函数__main_block_func_0的指针。
2.__main_block_impl_0结构体的Desc是指向结构体__main_block_desc_0_DATA的指针。
3.__main_block_impl_0结构体的Flags没有传值,那么就是默认值0。
4.__main_block_impl_0结构体的isa是&_NSConcreteStackBlock,后面详细说明。
函数__main_block_func_0
1.封装了Block内要执行的表达式代码。
2.参数就是__main_block_impl_0结构体地址。
结构体 __main_block_desc_0
1.成员变量 reserved 传入的是固定值0
2.成员变量 Block_size 传入的是结构体__main_block_impl_0的大小
从以上分析可以看出Block的本质是OC对象,内部封装了isa指针,调用函数,及自身大小。
带参数的Block的本质
int main(int argc, char * argv[]) {
@autoreleasepool {
void(^blk)(int)= ^(int a){
printf("Block\n a=%d\n",a);
};
blk(10);
}
return 0;
}
同样执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 并精简代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
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)};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a) {
printf("Block\n a=%d\n",a);
}
int main(int argc, char * argv[]) {
void(*blk)(int)= &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA));
((__block_impl *)blk)->FuncPtr)(blk, 10);
}
return 0;
}
我们看到,blk的调用和__main_block_func_0函数定义发生了变化都多出了一个int参数。
局部变量捕获
一、自动变量捕获:我们看下为什么打印出来的age是10而不是20.
int main(int argc, char * argv[]) {
@autoreleasepool {
//默认是自动变量 auto
int age = 10;
void(^blk)(void)= ^(void){
printf("Block\n age=%d\n",age);
};
age = 20;
blk();
}
return 0;
}
同样执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 并精简代码
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 _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
printf("Block\n age=%d\n",age);
}
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 argc, char * argv[]) {
int age = 10;
void(*blk)(void)=&__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age));
age = 20;
((__block_impl *)blk)->FuncPtr)(blk);
}
return 0;
}
我们看到结构体__main_block_impl_0的定义发生了变化,多了一个age的成员变量,并且age的赋值发生在构造函数__main_block_impl_0调用时,并且传进去的值是10。
再来看一下函数__main_block_func_0的实现,其内部有一个局部变量 int age = __cself->age;
可知这个age的值也是10。
后面的age=20;并不会影响__main_block_impl_0函数内部的局部变量age的值,所以打印出来的是10,而不是20。
二、静态局部变量捕获:
int main(int argc, char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
void(^blk)(void)= ^(void){
printf("Block\n age=%d\n height=%d",age,height);
};
age = 20;
height = 20;
blk();
}
return 0;
}
同样执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 并精简代码
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;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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
printf("Block\n age=%d\n height=%d",age,(*height));
}
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 argc, char * argv[]) {
int age = 10;
static int height = 10;
void(*blk)(void)= &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age,
&height));
age = 20;
height = 20;
((__block_impl *)blk)->FuncPtr(blk);
}
return 0;
}
static修饰的局部变量height,在结构体__main_block_impl_0定义时是指针int *height;
,并且构造函数调用时传入的是变量的地址&height
,在函数__main_block_func_0内也是通过地址取出内存中的值int *height = __cself->height;
,可以看出static修饰的变量捕获传递的是地址,后面对height = 20
,相应的内存发生了改变,在block内部通过内存地址取出的值也发生了改变。
那么思考下为什么会有这种差异呢,我们来看段代码。
void(^blk)(void);
void test(){
int age = 10;
static int height = 10;
blk= ^(void){
printf("Block\n age=%d\n height=%d",age,height);
};
age = 20;
height = 20;
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
blk();
}
return 0;
}
上述代码在离开test函数的作用域后,age变量会销毁,相应的内存被回收,那么blk()调用时如果是地址传递的话此时会发生坏内存访问。但static修饰的height变量为静态局部变量,height的作用域是test函数,但height的生命周期却是整个程序运行期间,指向的内存不会回收,是可以通过地址进行访问的。
三、self的捕获
创建一个Person类,Person.m文件内的代码如下
#import "Person.h"
@implementation Person
-(void)test{
void(^blk)(void)=^(void){
NSLog(@"self=%@",self);
};
blk();
}
@end
执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m 并精简代码
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
Person *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s1_dbzn9w0x6h121vpvtg1vfcf80000gn_T_person_50bb37_mi_0,self);
}
static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) {
_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) {
_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static struct __Person__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __Person__test_block_impl_0*, struct __Person__test_block_impl_0*);
void (*dispose)(struct __Person__test_block_impl_0*);
} __Person__test_block_desc_0_DATA = {
0,
sizeof(struct __Person__test_block_impl_0),
__Person__test_block_copy_0,
__Person__test_block_dispose_0
};
static void _I_Person_test(Person * self, SEL _cmd) {
void(*blk)(void)=&__Person__test_block_impl_0(
__Person__test_block_func_0,
&__Person__test_block_desc_0_DATA,
self,
570425344));
((__block_impl *)blk)->FuncPtr)(blk);
}
可以看出函数-(void)test
在底层转为了函数static void _I_Person_test(Person * self, SEL _cmd)
,多了两个参数self,_cmd,self指向调用者,_cmd是函数名,这两个都是局部变量(参数也是局部变量),block内部访问了self,所以同样会发生变量捕获,从结构体struct __Person__test_block_impl_0
的定义以及构造函数__Person__test_block_impl_0
调用都可以看出这一点。
如果我们将源代码改成这样呢
#import "Person.h"
@interface Person()
@property (nonatomic, strong) NSString * name;
@end
@implementation Person
-(void)test{
void(^blk)(void)=^(void){
NSLog(@"name=%@",self.name);
};
blk();
}
@end
执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m 并精简代码
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
Person *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s1_dbzn9w0x6h121vpvtg1vfcf80000gn_T_person_4e4ffa_mi_0,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
}
可以看到捕获的同样是self而不会去单独捕获_name变量,因为在函数调用时传进来的参数是self而不是_name,它只是self的一个成员变量。
全局变量访问
int age = 10;
static int height = 10;
int main(int argc, char * argv[]) {
@autoreleasepool {
void(^blk)(void)= ^(void){
printf("Block\n age=%d\n height=%d",age,height);
};
age = 20;
height = 20;
blk();
}
return 0;
}
同样执行指令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 并精简代码。
int age_ = 10;
static int height_ = 10;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n age=%d\n height=%d",age_,height_);
}
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 argc, char * argv[]) {
void(*blk)(void)= &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA));
age_ = 20;
height_ = 20;
((__block_impl *)blk)->FuncPtr(blk);
}
return 0;
}
可以看出全局变量并不会发生变量捕获。
为什么要有变量捕获?
因为全局变量在任何函数内都可以直接访问不需要捕获。局部变量需要捕获的原因是因为离开了函数作用域变量销毁或者不可再访问,这是其要发生捕获的根本原因。
Block的底层结构图
行者常至,为者常成!