1、block的底层结构
- 先给出结论:
block的本质是结构体,block内执行的方法是这个结构体里成员变量-匿名函数函数指针 - 验证一下:
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block1)(void) = ^{
NSLog(@"i'm a NSGlobalBlock block");
};
block1();
NSLog(@"%@", block1);
}
return 0;
}
编译main.m拿到c++源码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
拿到源码main.cpp:
/// block的结构体
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;
}
};
/// block内执行的方法,此刻是一个函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_6f_2snw6yxn2sz630d3r61gc0700000gn_T_main_a2088f_mi_0);
}
/// block内的实现结构体
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
/// 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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_6f_2snw6yxn2sz630d3r61gc0700000gn_T_main_a2088f_mi_1, block1);
/// 简化一下
/// 构造block结构体
block1 = __main_block_impl_0(__main_block_func_0, __main_block_desc_0_DATA);
/// 调用匿名函数
block1->funcPtr();
}
return 0;
}
由此可见,block的本质就是一个结构体,内部有两个结构体成员:第一个存储着结构体的isa指针和匿名函数指针,第二个存储着block的基本信息(size)。
因此我们得出block结构体基本上如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __block_info {
size_t reserved;
size_t Block_size;
};
typedef struct _SanjiBlock {
struct __block_impl impl;
struct __block_info info;
} *sanjiBlock;
我们可以用_SanjiBlock来强转以上的block
// 直接调用block
block1();
// 通过拿到函数指针来调用
sanjiBlock sanjiBlock = (__bridge void *)block1;
void (*p)(void) = sanjiBlock->impl.FuncPtr;
p();
//打印结果一样:
2020-12-09 14:35:15.207888+0800 BlockDetail[39554:1284881] i'm a NSGlobalBlock block
2020-12-09 14:35:15.207928+0800 BlockDetail[39554:1284881] i'm a NSGlobalBlock block
2、捕获(capture)、以及 __block
- 对局部变量的捕获(
auto,static)
int main(int argc, const char * argv[]) {
@autoreleasepool {
/// auto可省略
auto int age1 = 10;
void (^block1)(void) = ^{
NSLog(@"age1 is %d", age1);
};
age1 += 10;
block1();
static int age2 = 10;
void (^block2)(void) = ^{
NSLog(@"age2 is %d", age2);
};
age2 += 10;
block2();
}
return 0;
}
/// 打印结果
age1 is 10
age2 is 20
仍然编译main.m:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age1;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age1, int flags=0) : age1(_age1) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
int *age2;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int *_age2, int flags=0) : age2(_age2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看出,block1捕获了age1的值,block2捕获了age2的地址,原因很简单:
age1是auto局部变量,离开作用域就销毁了,所以block1只能捕获它的值(赶在销毁之前),而static修饰的age2是一只在内存中的,block2捕获它的地址,随时可以访问到最新的值。
-
Block会捕获全局变量么? 不会,因为全局变量一直在内存中,可以直接访问拿到最新的值。 -
所以,得出以下结论:
| 变量类型 | 是否会被block捕获 | 访问方式 |
|---|---|---|
| auto局部变量 | 会 | 值传递 |
| static局部变量 | 会 | 指针传递 |
| 全局变量 | 不会 | 直接访问 |
3、block的类型
| block类型 | 环境 |
|---|---|
| NSGlobalBlock | 没有访问auto变量 |
| NSStackBlock | 访问了auto变量 |
| NSMallocBlock | NSStackBlock调用copy |
上代码:(在ARC环境下)
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block1)(void) = ^{
NSLog(@"i'm block1");
};
int age = 10;
void (^block2)(void) = ^{
NSLog(@"age is %d", age);
};
void (^block3)(void) = [block2 copy];
NSLog(@"\nblock1 is %@\nblock2 is %@\nblock3 is %@\n", [block1 class], [block2 class], [block3 class]);
}
return 0;
}
/// 打印结果
block1 is __NSGlobalBlock__
block2 is __NSMallocBlock__
block3 is __NSMallocBlock__
那为什么block访问了auto变量,居然是__NSMallocBlock__, 那是ARC的作用,ARC把block从栈区拷贝到了堆区, 同时引用计数器+1如果block在栈区,那么离开作用域以后就会被释放,这个时候再去调用block就会产生不可预知的结果,在堆区则靠程序员管理内存。
修改环境为MRC:
block1 is __NSGlobalBlock__
block2 is __NSStackBlock__
block3 is __NSMallocBlock__
另外哪些情况下,block会被自动拷贝到堆区?
block当成方法返回值被返回的时候- 将
block赋值给__strong指针的时候 block作为cocoa API中含有usingBlock的参数时
4、__Block
__Block用于修改block内部无法修改的auto变量__Block修饰的变量会被包装成一个对象
4、block的内存管理
先从两段代码说开:ARC环境下
@interface OnePerson()
@property(nonatomic, assign) int age;
@end
@implementation OnePerson
- (void)dealloc
{
NSLog(@"OnePerson --- dealloc");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
OnePerson *person = [[OnePerson alloc] init];
void (^block)(void) = ^{
person.age = 10;
};
NSLog(@"%@", block);
NSLog(@"block即将销毁");
}
return 0;
}
/// 打印结果
2020-12-09 20:09:12.955196+0800 BlockDetail[55739:1517434] <__NSMallocBlock__: 0x10040c800>
2020-12-09 20:09:12.955546+0800 BlockDetail[55739:1517434] block即将销毁
2020-12-09 20:09:12.955596+0800 BlockDetail[55739:1517434] OnePerson --- dealloc
block捕获了auto变量person, 因此应该是NSStackBlock,但由于ARC环境下自动copy到了堆区, person对象延迟释放了,直到block被释放person对象才被释放,为什么呢?
再次窥探底层源码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
OnePerson *__strong person; /// 强引用!!!!!
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, OnePerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
person对象被捕获进去,是被__strong修饰符引用的,可见,堆区block对捕获的对象是强引用的!!!!
栈空间的block不会强引用捕获的对象,堆空间的block会强引用捕获的对象。
为啥栈区block不会强引用捕获变量???栈区的block自身难保,朝不保夕,随时要被释放,怎么还会去强引用捕获的变量呢。也就是说栈区的block不会retain捕获的外部变量