Block底层原理
Block底层数据结构
声明一个block
void (^block)(int, int) = ^(int a , int b){
};
通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp编译成C++代码,简化为下面的代码
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;
};
发现block本质也是一个OC对象,它内部也有isa指针
block是封装了函数调用以及函数调用环境的OC对象。
block执行执行逻辑
void (^block)(void) = ^{
NSLog(@"Hello, World!");
};
block();
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 构造函数(类似于OC的init方法),返回结构体对象
__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;
}
};
// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
}
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, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 定义block变量
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
将__main_block_func_0函数地址 通过__main_block_impl_0构造函数赋值给了FuncPtr,__main_block_impl_0封装了block执行的函数
// 执行block内部的代码
block->FuncPtr(block);
}
return 0;
}
Block变量捕获
可分为局部变量(又分为auto变量,和static变量),全局变量
int age_ = 10;
static int height_ = 10;
void (^block)(void);
void test()
{
//默认是auto:自动变量,离开作用域就销毁
auto int a = 10;
static int b = 10;
block = ^{
NSLog(@"age = %d, height = %d, a = %d, b = %d", age_, height_,a,b);
};
a = 20;
b = 20;
age_ = 20;
height_ = 20;
//结果age = 20, height = 20, a = 10, b = 20
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
编译成底层代码为
int age_ = 10;
static int height_ = 10;
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a; // 数值
int *b; // 指针
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
void test()
{
auto int a = 10;
static int b = 10;
block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, &b));
a = 20;
b = 20;
age_ = 20;
height_ = 20;
}
// block 执行代码
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_l4_53j9g79j49s6l4zzsk30ym7r0000gn_T_main_c4abf1_mi_0, age_, height_,a,(*b));
}
// 可以发现
1、block 初始化的时候,局部变量 a的值是直接传入到block 内部,然后,&b是传入的指针
2、全局变量age_,和height_,没有被传入到block内容,使用的时候是直接访问的
所以age_,height_ 和 b都能修改,a修改失败
3、这样设计的原因是因为,
】局部auto变量的作用域就是test方法内,生命周期也是,超过方法就会被释放,但是block底层是在另外一个函数使用a的,所以需要捕获到blcok内部,
】b是因为通过static修饰,虽然作用域也是test方法,但是生命周期是整个程序,所以在block执行函数里面,只需要捕获指针,然后通过指针访问就可以
】至于全局变量,作用域是全局的,在__test_block_func_0可以直接访问到,所以不需要捕获
block的类型以及存储区域
Snip20200708_6.png在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
1、block作为函数返回值时
typedef void (^LQBlock)(void);
LQBlock myblock()
{
int a = 10;
return ^{
NSLog(@"---------%d",a);
};
}
2、将block赋值给__strong指针时
int age = 10;
LQBlock block = ^{
NSLog(@"---------%d", age);
};
3、block作为Cocoa API中方法名含有usingBlock的方法参数时
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
4、block作为GCD API的方法参数时
dispatch_async(dispatch_queue_t _Nonnull queue, ^{
<#code#>
});
_block
_block可以用于解决block内部无法修改auto变量值的问题
_block不能修饰全局变量、静态变量(static)
编译器会将_block变量包装成一个对象
__block int age = 10;
MJBlock block = ^{
age = 20;
NSLog(@"age is %d", age);
};
编译成C++
struct __Block_byref_age_0 {
void *__isa;
struct __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;
struct __Block_byref_age_0 *age; // _block包装成的对象
};
内部修改age的代码
__Block_byref_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
发现修改age是通过先指向forwarding再指向age修改的,原因是,
block 一开始是在栈上的__forwarding指向栈上的自己,但是当block被拷贝到堆上时堆上的__block结构体内部的__forwarding指向自己,但是栈上的__forwarding 也指向堆上的__block 结构体。当我们修改age时候肯定希望把修改的值放到堆上,所以通过__forwarding第一个age即使是在栈上的,最终也是修改的堆上的age。