前言
无论在面试还是在工作中,总会碰到 block 是什么?block 循环引用怎么办?block 修饰符使用什么?等等这种类似的问题。
一、 什么是 block
一、Demo1
int main(int argc, const char * argv[]) {
@autoreleasepool {
/// 下面的代码,就是一个简单的 block
^{
NSLog(@"Hello Block");
};
}
return 0;
}
分析:上面的代码,就是一个最简单的 block 但是 NSLog 里面的代码不会被打印出来,因为这个 block 没人调用,所以永远不会执行。
二、Demo2
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 18;
void (^block)(void) = ^{
NSLog(@"age--%d", age);
};
block();
}
return 0;
}
分析:如上,运行程序,控制台会打印出 age--18。
三、分析 block 内部实现
- 通过 clang 编译可以将 OC 代码转化成 C++ 代码,来查看 block 底层的实现原理
- 在终端上输入
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
会在main.m 这级目录下生成一个 main.cpp 文件,就是我们想要的 转化后的代码
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 这个就是 block 内部的结构,
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// c++ 的构造函数(类似于 OC 的 init 方法),返回一个结构体对象。
// age(_age) 这句代码,就是把 _age 赋值给 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;
}
};
// 封装了 block 执行逻辑的函数,传入到 fp,fp 在赋值到 impl.funcPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__7ngz5gzx5sjgs4dlqrh7t58w0000gn_T_main_679898_mi_0, 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)};
// 这句代码对应我们上面的 main 函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 18;
// 定义 block 变量
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 执行 block 内部的代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
分析上面的代码,在 main 函数里我们对比,转化后的 C++ 代码和 原生的 OC 代码
图片.png__main_block_impl_0 这个结构体就是 block 的本来面目
__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// c++ 的构造函数(类似于 OC 的 init 方法),返回一个结构体对象。
// age(_age) 这句代码,就是把 _age 赋值给 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;
}
};
上面这个结构体的构造函数,传入的参数:void *fp、struct __main_block_desc_0 *desc、int _age、int flags=0
由于 int flags=0 传入的是常亮数值 0 因此可以忽略,int _age 这个参数是因为外面定义的 局部变量,也可以忽略,所以 这个构造函数的必须参数有两个 void *fp、struct __main_block_desc_0 *desc
上面的 C++ 代码中在 可以看出 void *fp 对应的是 &__main_block_impl_0、struct __main_block_desc_0 *desc 对应的是 &__main_block_desc_0_DATA ,fp又赋值到了imp里面的 FuncPtr,desc 赋值到 Desc 就是 当前结构体的 Desc。
__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__7ngz5gzx5sjgs4dlqrh7t58w0000gn_T_main_679898_mi_0, age);
}
上面是 block 执行逻辑的函数,传入到 fp,fp 在赋值到 impl.funcPtr
__block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__main_block_desc_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)};
这个结构体存了 block 的大小。
四、Demo3 捕获 auto 变量的 block
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 18;
void (^block)(void) = ^{
NSLog(@"age--%d", age);
};
age = 20;
block();
}
return 0;
}
如上,我们修改了局部变量的 age 修改为20,但是运行后,block 里面打印的结果仍为 18.
- 这是因为 block 捕获了 age = 18,进行了值传递,相当于直接把 age= 18 赋值给了 age, 无论外面怎么修改,都不会改变 age 的值,
- C 语言会在我们定义局部变量的时候,自动给我们的属性加上 auto 修饰
五、Demo4 捕获 static 变量的 block
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 18;
static int height = 170;
void (^block)(void) = ^{
NSLog(@"age--%d,height--%d", age, height);
};
age = 20;
height = 180;
block();
}
return 0;
}
修改 height 的值,运行代码,结果为 age--18,height--180
这是为什么呢,再次编译运行生成 C++ 代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 18;
static int height = 170;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 180;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
从上面的 C++ 代码可以看出传递给block 的height 是地址传递,指针传递
- block 内部捕获的是 *height 而不是 height。
六、Demo5 全局变量
int age = 22;
int height = 170;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"age--%d,height--%d", age, height);
};
age = 20;
height = 180;
block();
}
return 0;
}
运行上面的代码,打印结果为 age--20,height--180
,同样,我们分析 生成的 C++ 代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age = 20;
height = 180;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
- block 并没有捕获 任何全局变量
七、总结
一、block 的内存结构
图片.png 图片.png二、block 本质
- block 本质上也是一个 OC 对象,它内部也有一个 isa 指针
- block 是封装了函数调用以及函数调用环境的 OC 对象
二、block 的变量捕获
为了保证 block 内部能正常访问外部的变量,block 有一个变量捕获机制
- 如果变量的类型是局部变量,无论是 auto 还是 static 修饰,都会被捕获到 block 内部,但是 auto 变量是值传递,static 是指针传递。
- 如果变量类型是全局变量,不会被 block 捕获到内部,直接使用。
变量类型 | 是否能捕获到 block 内部 | 访问方式 |
---|---|---|
局部变量 auto | 是 | 值传递 |
局部变量 static | 是 | 指针传递 |
全局变量 | 否 | 直接访问 |
为什么 block 要捕获局部变量的值呢,auto:自动变量,离开作用域就销毁
三、block 的类型
一、block 的对象特性
我们知道 block 的本质就是OC 对象,所以OC对象的一些方法同样适用于 block
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
}
return 0;
}
2020-12-20 16:55:50.239327+0800 MyBlock[18424:5457366] __NSGlobalBlock__
2020-12-20 16:55:50.239799+0800 MyBlock[18424:5457366] __NSGlobalBlock
2020-12-20 16:55:50.239839+0800 MyBlock[18424:5457366] NSBlock
2020-12-20 16:55:50.239871+0800 MyBlock[18424:5457366] NSObject
得出继承关系为 NSGlobalBlock -> __NSGlobalBlock -> NSBlock ->NSObject
二、block 的类型查询
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
// 堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存
void (^block1)(void) = ^{
NSLog(@"Hello");
};
int age = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", age);
};
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
NSLog(@"%d", age);
} class]);
}
return 0;
}
在ARC 环境下:
2020-12-20 17:00:31.886757+0800 MyBlock[18515:5460672] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
在 MRC 环境下:
2020-12-20 17:26:28.303551+0800 MyBlock[19030:5476820] __NSGlobalBlock__ __NSStackBlock__ __NSStackBlock__
在 MRC 环境下使用 copy
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^block2)(void) = [^{
NSLog(@"Hello - %d", age);
} copy];
}
return 0;
}
同样在 MRC 环境下,进行 copy 操作后打印结果和 ARC 环境下一样
2020-12-20 17:27:42.096753+0800 MyBlock[19064:5478650] __NSMallocBlock__
如上,我们看打印结果得知 block 有 3 种类型,可以通过调用 class 方法或者 isa 指针查看具体类型,最终都是继承自 NSBlock 类型
__NSGlobalBlock__
(_NSConcreteGlobalBlock)没有访问 auto 变量
__NSMallocBlock__
(_NSConcreteMallocBlock)__NSStackBlock__
调用了 copy
__NSStackBlock__
(_NSConcreteStackBlock)访问了 auto 变量,(在ARC环境下显示NSMallocBlock,在 MRC 环境下,显示 NSStackBlock)
每一种类型的 block 调用 copy 后的结果如下:
Block 的类 | 副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数 + 1 |
内存分布
图片.png四、block 的 copy
一、自动复制
在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上
1、block 作为函数返回值
typedef void (^MyBlock)(void);
MyBlock myblock() {
return ^{
NSLog(@"我被调用了");
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock block = myblock();
block();
}
return 0;
}
会打印出结果,如果没有进行 copy
2、将 block复制给 __strong 指针时
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
MyBlock block = ^{
NSLog(@", %d", age);
};
block();
NSLog(@"%@", [block class]);
}
return 0;
}
打印 [block class] 显示为NSMallocBlock ,证明是NSStackBlock copy 之后的类型。
3、block 作为 Cocoa API 中方法名含有 usingBlock的方法参数时
// 如 NSArray 里面的函数, 此时的 block 都是在堆上的,都是进行了 copy 的
NSArray *arr = @[];
[arr sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
}];
4、block 作为GCD API 的方法参数时
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
二、block属性的建议写法
1、MRC 环境下
- @property (copy, nonatomic) void (^block)(void);
2、ARC 环境下
- @property (strong, nonatomic) void (^block)(void);
- @property (copy, nonatomic) void (^block)(void);
三、对象类型的 auto 变量
1、当 block内部访问了对象类型的 auto 变量时
#import "MyPerson.h"
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock block;
{
MyPerson *person = [[MyPerson alloc] init];
person.age = 18;
block = ^{
NSLog(@"-----person.age===%d", person.age);
};
}
NSLog(@"-----");
}
return 0;
}
转化C++ 代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MyPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
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};
- 如果 block 是在栈上
- 将不会对 auto 变量产生强引用。
- 如果 block 被拷贝到堆上
- 会调用 block 内部的 copy 函数
- copy 函数内部会调用 _Block_object_assign 函数
- _Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)作出相对应的操作,形成强引用或者弱引用
- 如果 block 从堆中移除
- 会调用 block 内部的dispose 函数
- dispose 函数内部会调用 _Block_object_dispose 函数
- _Block_object_dispose 函数会自动释放引用的 auto 变量
函数 | 调用时机 |
---|---|
copy 函数 | 栈上的 block 复制到堆时 |
dispose 函数 | 堆上的 block 被废弃时 |
把上面的 person 使用 __weak 修饰
__weak MyPerson *weakPerson = person;
block = ^{
NSLog(@"-----person.age===%d", weakPerson.age);
};
使用 __weak 修饰时,在使用clang转换OC为C++代码时,可能会遇到以下问题
/var/folders/f_/7ngz5gzx5sjgs4dlqrh7t58w0000gn/T/main-643d6e.mi:28880:28: error:
cannot create __weak reference because the current deployment target does
not support weak references
__attribute__((objc_ownership(weak))) MyPerson *weakPerson = person;
^
1 error generated.
针对 cannot create __weak reference in file using manual reference 这个问题,解决方案:支持ARC、指定运行时系统版本,比如增加 -fobjc-arc -fobjc-runtime=ios-8.0
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
MyPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, MyPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
五、__block 修饰符
执行 下面的代码,显然会报错,从上面的代码中得知,block 中的 age 是block内部的,想要main 函数中的 age 是不可能的。
typedef void (^MyBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 14;
MyBlock block = ^{
age = 18;//报错 Variable is not assignable (missing __block type specifier)
NSLog(@"%d", age);
};
}
return 0;
}
如果 block 想要修改内部的变量,可以使用 static 或者 全局变量,但是,
- 这种做法会一直占用内存的空间
- 使用 static 修饰,会修改 block的类型,使用 static 修饰后,block 的类型变成了 ”NSGlobalBlock“,之前为”NSMallocBlock“
一、使用__block 修饰,修改变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 14;
MyBlock block = ^{
age = 18;
NSLog(@"%d", age);
};
block();
NSLog(@"%@", [block class]);
}
return 0;
}
使用 __block 修饰后,block 的类型 依旧为 ”NSMallocBlock“,使用后发现 block 内部 有一个 __Block_byref_age_0 引用这 age这个指针,__Block_byref_age_0 里面有一个 age 变量,我们修改 age 其实就是修改 __Block_byref_age_0 里面的 age这个值。__forwarding 这个指针时指向自己的一个 指针。
struct __Block_byref_age_0 {
void *__isa;
__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;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20; // 真正修改 age 的地方
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__7ngz5gzx5sjgs4dlqrh7t58w0000gn_T_main_f41d15_mi_0, (age->__forwarding->age));
}
二、__block 修饰符 原理
- __block 可以用于解决 block 内部无法修改 auto 变量值得问题
- __block 不能修饰全局变量、静态变量
- 使用 ____block 修饰时,编译器会将 ____block 变量包装成一个对象。
三、___block 的内存管理
- 当 block 在栈上时,并不对 __block 变量产生强引用
- 当 block 在堆上时
- 会调用 block 内部的 copy 函数
- copy 函数内部会调用 _Block_object_assign 函数
- _Block_object_assign 函数会对 __block 变量形成强引用
- 当block 从堆中移除时
- 会调用 block 内部的dispose 函数
- dispose 函数内部会调用 _Block_object_dispose 函数
- _Block_object_dispose 函数会自动释放引用的 __block 变量
四、__block 的 forwarding 指针
[图片上传失败...(image-a5af69-1608559220453)]
五、对象类型的 auto 变量、__block 变量
-
当 block 在栈上时,对它们都不会产生强引用
-
当 block 拷贝到堆上时,都会通过 copy 函数来处理它们
- __block变量(假设变量名叫做a)
- _Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
-
对象类型的 auto 变量(假设变量名叫做p)
- _Block_object_assign((void)&dst->p, (void)src->p, 3/BLOCK_FIELD_IS_OBJECT/);
-
当 block 从堆上移除时,都会通过 dispose 函数来释放它们
- __block变量(假设变量名叫做a)
- _Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF*/);
-
对象类型的auto变量(假设变量名叫做p)
- _Block_object_dispose((void)src->p, 3/BLOCK_FIELD_IS_OBJECT*/);
-
对象 BLOCK_FIELD_IS_OBJECT __block 变量 BLOCK_FIELD_IS_BYREF
六、被__block 修饰的对象类型
- 当__block变量在栈上时,不会对指向的对象产生强引用
- 当__block变量被copy到堆时
- 会调用__block变量内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)
- 如果__block变量从堆上移除
- 会调用__block变量内部的dispose函数
- dispose函数内部会调用_Block_object_dispose函数
- _Block_object_dispose函数会自动释放指向的对象(release)
六、循环引用
一、什么是循环引用
图片.pngint main(int argc, const char * argv[]) {
@autoreleasepool {
MyPerson *p = [[MyPerson alloc] init];
p.age = 18;
p.block = ^{
NSLog(@"age is %d", p.age);
};
NSLog(@"-------");
}
return 0;
}
// MyPerson
typedef void (^MyBlock) (void);
@interface MyPerson : NSObject
@property (copy, nonatomic) MyBlock block;
@property (assign, nonatomic) int age;
@end
上面的代码产生了循环引用,block 里面的代码 无法执行
二、解决循环引用
一、ARC 环境
1、使用 __weak
- 不会产生强引用,指向的对象销毁时,会自动让指针置为nil
MyPerson *p = [[MyPerson alloc] init];
__weak typeof(p) weakP = p;
p.age = 18;
p.block = ^{
NSLog(@"age is %d", weakP.age);
};
2、使用 __unsafe_unretained
- 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变
MyPerson *p = [[MyPerson alloc] init];
__unsafe_unretained typeof(p) weakP = p;
p.age = 18;
p.block = ^{
NSLog(@"age is %d", weakP.age);
};
3、使用 __block
- 使用 __block 必须 调用 block,并把对象置为 nil
__block MyPerson *p = [[MyPerson alloc] init];
p.age = 18;
p.block = ^{
NSLog(@"age is %d", p.age);
p = nil;
};
p.block();
二、MRC 环境
由于 MRC 环境不支持 __weak,所以只有两种情况
1、__unsafe_unretained
- 和 ARC 一样
2、__block
- 不需要置为 nil 和 手动调用 block
网友评论