在做项目的时候常用到block, 最近看了一些资料, 对block的有了更深入的理解, 下面记录下。
一、Block底层结构
先看一个简单的block
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void (^block_test)(void) = ^{
NSLog(@"Hello, World!");
};
block_test();
}
return 0;
}
下面把OC代码转成C++代码, 打开终端执行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
, 打开main-arm64.cpp文件
先精简一下, 删减部分强制转换类型代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
// // 定义blockTest
void (*blockTest)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
// 调用blockTest
blockTest->FuncPtr(blockTest);
}
return 0;
}
先看下blockTest是怎么实现的,
找到__main_block_impl_0
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;
}
};
结构体中有个__main_block_impl_0()
构造函数, 类似于OC的init方法, 从构造函数的实现代码可以看出是把 传入的参数赋值给impl
和Desc
。
看下__block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
isa
指针, 说明 block是一个对象
FuncPtr
函数指针 指向block要执行的内容
看下构造函数__main_block_impl_0()
传入的参数
1.__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// blockTest 执行的内容
NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_rp70pg912_z5my021550_xx00000gn_T_main_6c65c0_mi_0);
}
通过NSLog
可以看出它是一个内部封装了 block执行逻辑 的函数。
2.&__main_block_desc_0_DATA
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_block_desc_0_DATA
是一个__main_block_desc_0
结构体变量, 它有两个参数
-
reserved
: 0 -
Block_size
:sizeof(struct __main_block_impl_0)
block占用内存空间大小
阶段总结:
-
- block本质上也是一个OC对象, 它内部也有个isa指针
-
- block是封装了函数调用以及函数调用环境的OC对象
Block底层结构可以用一张图展示
二、Block变量捕获
为了保证block内部能够正常访问外部的变量, block有个变量捕获机制。
1. auto变量(自由变量、局部变量)
局部变量int age=10;
默认是带auto
的, 就是auto int age=10;
先看下面代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void (^blockTest)(void) = ^{
NSLog(@"age is %d", age);
};
age = 20;
blockTest();
}
return 0;
}
运行看下, 打印结果 age is 10
在调用blockTest()之前已经把age修改成20, 但为什么会打印结果是10呢?
用clang命令, 看下c++代码
int main(int argc, const char * argv[]) {
{ __AtAutoreleasePool __autoreleasepool;
int age = 10;
void (*blockTest)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);
age = 20;
blockTest->FuncPtr(blockTest);
}
return 0;
}
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_rp70pg912_z5my021550_xx00000gn_T_main_1c5279_mi_0, age);
}
发现block里也有个变量int age
, 也就是说变量age被block捕获, 而且是值传递
2. static局部变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int hight = 10;
void (^blockTest)(void) = ^{
NSLog(@"age is %d, hight is %d", age, hight);
};
age = 20;
hight = 20;
blockTest();
}
return 0;
}
打印结果: age is 10, hight is 20
看下c++代码
// main函数中, 定义block
void (*blockTest)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age, &hight);
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *hight;
}
可以看到block里有个指针int *hight
, 也就是说static变量hight也被block捕获了, 但是是指针传递, static修饰的变量,在内存中只有一份, 所以打印的是hight修改过的值。
全局变量
int age = 10;
static int hight = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^blockTest)(void) = ^{
NSLog(@"age is %d, hight is %d", age, hight);
};
age = 20;
hight = 20;
blockTest();
}
return 0;
}
打印结果: age is 20, hight is 20
C++代码
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没有捕获, 可以直接访问。
阶段总结:
-
- 局部变量因为跨函数访问, 需要被block捕获
-
- 自由(局部)变量是值传递, static局部变量是指针传递
-
- 全局变量在每个函数都能访问, 不需要捕获
思考下在block中访问, 成员变量_weight, 会不会捕获 ?
直接用代码验证
@interface Person : NSObject
//@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger weight;
- (void)test;
@end
@implementation Person
- (void)test
{
void (^blockTest)(void) = ^{
// NSLog(@"name is %@", _name);
NSLog(@"weight id %ld", _weight);
};
blockTest();
}
@end
下面看下C++代码
static void _I_Person_test(Person * self, SEL _cmd) {
void (*blockTest)(void) = &__Person__test_block_impl_0(__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344);
blockTest->FuncPtr(blockTest);
}
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;
}
};
下面是block执行内容 --- NSLog
1. 只访问_weight时
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_6l_rp70pg912_z5my021550_xx00000gn_T_Person_4ac00c_mi_0, (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_weight)));
}
2. 只访问_name时
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_6l_rp70pg912_z5my021550_xx00000gn_T_Person_a282fe_mi_0, (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)));
}
可以看出访问_name、_weight时, block定义时都只传入了参数self
和570425344
, block结构体中有个Person *self
, 没有_name和_weight, 所以
在block中使用成员变量, block会捕获self。
**解释:**
从函数static void _I_Person_test(Person * self, SEL _cmd)
可以知道-(void)test
方法默认有两个参数Person * self, SEL _cmd
, 所以 self 在test方法中是一个局部变量, 再看下blockTest定义 &__Person__test_block_impl_0(__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344);
传入的第三个参数是就是 局部变量self
, 上面已经验证了局部变量因为跨函数访问, 会被block捕获
三、Block类型
block有三种类型, 可以通过class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
- NSGlobalBlock ---
block没有访问auto变量
void (^block1)(void) = ^{
NSLog(@"block1");
};
NSLog(@"block1 %@", [block1 class]) ;
打印结果: block1 __NSGlobalBlock__
没有访问auto变量, 并且有指针强引用, 储存在全局区
- NSStackBlock ---
block访问auto变量
NSLog(@"block3 %@", [^{
NSLog(@"block3 age = %d", age);
} class]);
打印结果: block3 __NSStackBlock__
访问了auto变量,没有指针强引用,直接调用block块, 存放在栈区
- NSMallocBlock ---
对NSStackBlock执行copy操作
int age = 10;
void (^block3)(void) = ^{
NSLog(@"block3 age = %d", age);
};
NSLog(@"block3 %@", [block2 class]) ;
打印结果: block3 __NSMallocBlock__
访问了auto变量, 并且有指针强引用, 储存在堆。
到这里可能会有疑问, block访问auto变量
应该是NSStackBlock类型的, 存放到栈区才对吧?
理论上的确应该是如此。但上面代码是在ARC下运行的结果, 编译器会根据具体情况将 栈上的block拷贝到堆上
切换到MRC下运行试试
打印结果: block3 __NSStackBlock__
可以看到在MRC环境下是NSStackBlock类型的
下面手动执行下copy操作
int age = 10;
void (^block3)(void) = [^{
NSLog(@"block3 age = %d", age);
} copy];
NSLog(@"block3 %@", [block3 class]) ;
打印结果: block3 __NSMallocBlock__
阶段总结:
-
- 没有访问auto变量, block是NSGlobalBlock类型
-
- 访问了auto变量, block是NSStackBlock类型
-
- NSStackBlock类型的block调用了copy, block是NSMallocBlock类型
-
- 在ARC环境下,编译器会根据具体情况将栈上的block拷贝到堆上
- a. block作为函数返回值时
- b. 将block赋值给__strong指针时
- c. block作为Cocoa API中方法名含有usingBlock的方法参数时
- d. block作为GCD API的方法参数时
- 在ARC环境下,编译器会根据具体情况将栈上的block拷贝到堆上
所以在MRC下block属性建议写法
@property (nonatomic, copy) void (^block)(void);
在ARC下block属性建议写法, 两者均可
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, strong) void (^block)(void);
四、Block访问对象类型的auto变量
Person *person = [[Person alloc] init];
person.weight = 80;
void (^blockTest)(void) = ^{
NSLog(@"weight id %ld", person.weight);
};
看C++代码
找到__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
}
person对象被block捕获, 但是带着关键字__strong。
把person用关键字__weak修饰试下
__weak Person *weak = person;;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weak;
}
发现捕获的person也是带着关键字__weak的
再看下__Person__test_block_desc_0
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};
发现多了两个函数指针void *copy
和 void *dispose
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*/);
}
当block内部访问了对象类型的auto变量时
- 如果block被拷贝到堆上
* 会调用block内部的copy函数
* copy函数内部会调用_Block_object_assign函数,根据
auto变量的修饰符(__strong,__weak), 形成强引用(retain)弱引用 - 如果block是从堆上移除
* 会调用block内部的dispose函数
* dispose函数内部会调用_Block_object_dispose函数,自动释放
(release)引用的auto变量 - 如果block是在栈上,不会对auto变量产生强引用
五、总结:
-
- block访问全局变量,不会捕获变量, 直接访问
(数据类型int或NSInteger、自定义对象类型如Person、系统对象类型如NSString)
- block访问全局变量,不会捕获变量, 直接访问
-
- block访问auto变量, 会捕获变量, 是值传递
- 对象类型的auto变量, 会调用copy函数强引用或弱引用变量,调用dispose函数释放引用的变量
(自定义对象类型如Person、系统对象类型如NSString) - 数据类型auto变量, 不会调用copy、dispose函数
- 对象类型的auto变量, 会调用copy函数强引用或弱引用变量,调用dispose函数释放引用的变量
- block访问auto变量, 会捕获变量, 是值传递
-
- block访问static局部变量, 会捕获变量, 是指针传递
- 数据类型static局部变量, 不会调用copy、dispose函数
(数据类型int或NSInteger) - 对象类型的static局部变量, 会调用copy、dispose函数
(自定义对象类型如Person、系统对象类型如NSString)
- 数据类型static局部变量, 不会调用copy、dispose函数
- block访问static局部变量, 会捕获变量, 是指针传递
以上就是关于Block底层结构、变量捕获机制的理解, 后续有新的会补充进来。