Block的本质
- 利用下面这条命令就可以将main.m转换成main.cpp察看底层的数据结构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
main.m
int a = 11;
void (^block) (void) = ^ {
NSLog(@"test-block-%d", a);
};
block()
main.cpp
// 从这里可以看出来,传进去的是值,而不是指针
int a = 11;
void (*block) (void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a));
// 为什么可以这样调用?而不是 block.imp->FunPtr(block)
// 因为 block 是 __main_block_impl_0 类型
// 第一个参数是__block_impl,内存首地址是一样的
// 所以可以__main_block_impl_0是可以强转成__block_impl直接取值的
block->FunPtr(block);
// 会将局部变量捕捉到内部
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
// 构造函数
__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) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_4bjr2jtj49b4zd1fpf8mmvq80000gn_T_main_a185df_mi_0, a);
}
struct __block_impl {
void *isa; // 和普通对象的 isa 是一样的
int Flags;
int Reserved;
void *FuncPtr; // 函数地址
};
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)};
由此可以得出结论,block就是封装了函数调用及其上下文的OC对象
变量捕捉(Captrue)
为了保证 block 内部能够正常访问外部的变量,block 有个变量捕获机制(跨函数调用肯定要捕获的,不然另一个函数怎么访问呢),捕获的是变量类型及其修饰符
| 变量类型 | 捕获到block内部 | 访问方式 |
|---|---|---|
| 局部变量auto | ✔ | 值传递 |
| 局部变量staic | ✔ | 引用传递 |
| 全局变量 | ❌ | 直接访问 |
因为局部变量 auto 是在栈空间的,调用完有可能访问不到他的值了,所以只能捕获他的值
而局部变量 static 调用完还可以访问到他的值,也就可以捕获指针了
而全局变量的内存是在数据段的,始终都是能访问的,也就没有必要捕获了
block 的类型
可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类型
| block类型 | 存放内存位置 | 情况 | 执行copy操作 |
|---|---|---|---|
| NSGlobalBlock | 放在数据段 | 没有访问auto变量 | 不变 |
| NSMallocBlock | 放在堆中 | NSStackBlock调用copy | 引用计数+1 |
| NSStackBlock | 放在栈中 | 访问了auto变量 | 变成__MallocBlock__ |
思考:如果在ARC环境下,如果仅捕获auto变量,为什么是__NSMacllocBlock?
block 的 copy
在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如以下情况
- block作为函数返回值时(类似masonry中的那样)
- block赋值给__strong指针时(这个就是为什么平时创建的一个block,看似在栈中,打印class却是mallocblock的原因)
- 系统级别的 API,比如usingBlock、GCD等
block 捕捉对象类型的 auto 变量
- block 内部访问了对象类型的 auto 变量时
- 如果 block 是在栈上,将不会对 auto 变量产生强引用
- 如果 block 在堆上,会调用内部的 copy 函数
- 如果 block 在堆上被移除,会调用内部的dispose函数
- __weak问题解决
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-runtime=ios-8.0.0 main.m
HXBlock block;
HXBlock block1;
{
Person *person = [[Person alloc] init];
__weak Person* weakPerson = person;
block = ^ {
[weakPerson class];
};
block();
}
{
Person *person = [[Person alloc] init];
block1 = ^ {
[person class];
};
block1();
}
NSLog(@"---------------");
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
// 可以根据person的修饰符来决定对外部的person是强引用还是弱引用
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
// 相当于 release
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
上面的desc里面会增加两个方法,一个是copy,一个是dispose
也可以看出如果捕获的是对象是其类型及修饰符。也就是__weak也被捕获了。
__block
- 想修改外部变量可以用 static、全局变量,但是会一直在内存中
- 为了解决block内部无法修改auto变量值的问题
- 会将 __block 变量包装成一个对象,里面有一个__forwarding指针会指向自身
- 也就是说 __block 修饰的对象也会进行内存管理
- 和普通的对象很接近,但是要注意__forwarding指针
- 当copy到堆上,__forwarding会指向堆的地址
main.m
__block int a = 11;
void (^block) (void) = ^ {
a = 20;
NSLog(@"test-block-%d", a);
};
block();
main.cpp
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};
void (*block) (void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344);
block->FuncPtr(block);
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_5f_4bjr2jtj49b4zd1fpf8mmvq80000gn_T_main_52d534_mi_0, (a->__forwarding->a));
}
循环引用
Person.m
- (void)test {
_block = {
self.age
}
}
由于变量捕捉的原因,所以如果在 block 中强引用了 self,而 self 也强引用了 block,所以出现了循环引用,就无法释放。
- 解决方案
- 使用 __weak/__unsafe_unretained 修饰 self,__weak typeof(self) weakSelf = self。这个 typeof 是编译器的特性,会自动识别你的类型,相当于 __weak Person *weakSelf = self;区别就是前者会自动置为 nil,而后者是不会的
- 使用 __block,因为 __block 会自动把变量封装成 __Block_byref_a_0 的结构体,而当被拷贝到堆上的时候,__forwarding 指针就指向堆上的内存,在执行完 block 的时候,将 self 置为 nil,就可以解决循环引用了。不发生循环引用的前提就是一定要执行 block,且在 block中将指针断开。
思考
- weakSelf 和 strongSelf
__weak __typeof(self)weakSelf = self; //1
[self.context performBlock:^{
[weakSelf doSomething]; //2
__strong __typeof(weakSelf)strongSelf = weakSelf; //3
[strongSelf doAnotherSomething];
}];
strongSelf 就是为了强引用 block 中捕获的弱引用变量。strongSelf 相当于只是函数内部定义的一个局部变量,他的作用就是为了保住 weakSelf 的命,不至于让 weakSelf 提前被释放。
- 可以使用 block 实现弱关联对象。