原文地址
block总结(二)
前言
在文章之前,先抛出如下问题。
1.block的原理是怎样的?本质是什么?
2.__block的作用是什么?有什么使用注意点?
3.block的属性修饰词为什么是copy?使用block有哪些使用注意?
4.block一旦没有进行copy操作,就不会在堆上
5.block在修改NSMutableArray,需不需要添加__block?
如果现在不是很熟悉,希望看完这篇文章,能有个新的认识。
导读
本文主要从如下几个方面讲解block
1.block的基本使用
2.block在内存中的布局
3.block对变量的捕获分析
4.MRC和ARC的对比
5.__block的分析
6.block中内存管理问题
7.block导致的循环引用问题
什么是block
先介绍一下什么是闭包。在 wikipedia 上,闭包的定义是
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻译过来表达就是
闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。
block 实际上就是 Objective-C 语言对于闭包的实现。
block的基本使用
1.block本质上也是一个OC对象,它内部也有个isa指针
2.block是封装了函数调用以及函数调用环境的OC对象
3.block的底层结构如下图
无参无返回值的定义和使用
//无参无返回值 定义 和使用
void (^MyBlockOne)(void) = ^{
NSLog(@"无参无返回值");
};
// 调用
MyBlockOne();
无参有返回值的定义和使用
// 无参有返回值
int (^MyBlockTwo)(void) = ^{
NSLog(@"无参有返回值");
return 2;
};
// 调用
int res = MyBlockTwo();
有参无返回值的定义和使用
//有参无返回值 定义
void (^MyBlockThree)(int a) = ^(int a){
NSLog(@"有参无返回值 a = %d",a);
};
// 调用
MyBlockThree(10);
有参有返回值的定义和使用
//有参有返回值
int (^MyBlockFour)(int a) = ^(int a){
NSLog(@"有参有返回值 a = %d",a);
return a * 2;
};
MyBlockFour(4);
typedef 定义Block
实际开发中,经常需要把block作为一个属性,我们可以定义一个block
eg:定义一个有参有返回值的block
typedef int (^MyBlock)(int a, int b);
定义属性的时候,如下即可持有这个block
@property (nonatomic,copy) MyBlock myBlockOne;
block实现
self.myBlockOne = ^int(int a, int b) {
return a + b;
};
调用
self.myBlockOne(2, 5);
block 类型和数据结构
block 数据结构分析
生成cpp文件
如下代码
int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
block();
打开终端,cd到当前目录下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成main.cpp
block 结构分析
int age = 20;
// 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);
上面的代码删除掉一些强制转换的代码就就剩下如下所示
int age = 20;
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age
);
// block的调用
block->FuncPtr(block);
看出block的本质就是一个结构体对象,结构体__main_block_impl_0
代码如下
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
//构造函数(类似于OC中的init方法) _age是外面传入的
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
//isa指向_NSConcreteStackBlock 说明这个block就是_NSConcreteStackBlock类型的
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
结构体中第一个是struct __block_impl 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_impl_0 占用的内存大小
}
结构体中第三个是age
也就是捕获的局部变量 age
__main_block_func_0
//封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_7f3f1b_mi_0,age);
}
用一幅图来表示
变量捕获
其实上面的代码我们已经看得出来变量捕获了,这里继续详细分析一下
变量类型 | 捕获到block内部 | 访问方式 |
---|---|---|
局部变量 auto | √ | 值传递 |
局部变量 static | √ | 指针传递 |
全局变量 | × | 直接访问 |
局部变量auto(自动变量)
我们平时写的局部变量,默认就有 auto(自动变量,离开作用域就销毁)
运行代码
例如下面的代码
int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 25;
block();
等同于
auto int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 25;
block();
输出
20
分析
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成main.cpp
如图所示
int age = 20;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
age = 25;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_d36452_mi_5);
可以知道,直接把age的值 20传到了结构体__main_block_impl_0中,后面再修改age = 25并不能改变block里面的值
局部变量 static
static修饰的局部变量,不会被销毁
运行代码
eg
static int height = 30;
int age = 20;
void (^block)(void) = ^{
NSLog(@"age is %d height = %d",age,height);
};
age = 25;
height = 35;
block();
执行结果为
age is 20 height = 35
可以看得出来,block
外部修改height
的值,依然能影响block内部的值
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成main.cpp
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_3146e1_mi_4,age,(*height));
}
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;
static int height = 30;
int age = 20;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 25;
height = 35;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
如图所示,age
是直接值传递,height
传递的是*height 也就是说直接把内存地址传进去进行修改了。
全局变量
运行代码
int age1 = 11;
static int height1 = 22;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"age1 is %d height1 = %d",age1,height1);
};
age1 = 25;
height1 = 35;
block();
}
return 0;
}
输出结果为
age1 is 25 height1 = 35
分析
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成main.cpp
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_4e8c40_mi_4,age1,height1);
}
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 (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age1 = 25;
height1 = 35;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
如图所示,age
是直接值传递,height
传递的是*height
也就是说直接把内存地址传进去进行修改了。
全局变量
运行代码
int age1 = 11;
static int height1 = 22;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"age1 is %d height1 = %d",age1,height1);
};
age1 = 25;
height1 = 35;
block();
}
return 0;
}
输出结果为
age1 is 25 height1 = 35
分析
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成main.cpp
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x4_920c4yq936b63mvtj4wmb32m0000gn_T_main_4e8c40_mi_4,age1,height1);
}
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 (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
age1 = 25;
height1 = 35;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
从cpp文件可以看出来,并没有捕获全局变量age1和height1,访问的时候,是直接去访问的,根本不需要捕获
小结
变量类型 | 捕获到block内部 | 访问方式 |
---|---|---|
局部变量 auto | √ | 值传递 |
局部变量 static | √ | 指针传递 |
全局变量 | × | 直接访问 |
1.auto修饰的局部变量,是值传递
2.static修饰的局部变量,是指针传递
其实也很好理解,因为auto修饰的局部变量,离开作用域就销毁了。那如果是指针传递的话,可能导致访问的时候,该变量已经销毁了。程序就会出问题。而全局变量本来就是在哪里都可以访问的,所以无需捕获。
block类型
block也是一个OC对象
在进行分析block类型之前,先明确一个概念,那就是block中有isa指针的,block是一个OC对象,例如下面的代码
void (^block)(void) = ^{
NSLog(@"123");
};
NSLog(@"block.class = %@",[block class]);
NSLog(@"block.class.superclass = %@",[[block class] superclass]);
NSLog(@"block.class.superclass.superclass = %@",[[[block class] superclass] superclass]);
NSLog(@"block.class.superclass.superclass.superclass = %@",[[[[block class] superclass] superclass] superclass]);
输出结果为
iOS-block[18429:234959] block.class = __NSGlobalBlock__
iOS-block[18429:234959] block.class.superclass = __NSGlobalBlock
iOS-block[18429:234959] block.class.superclass.superclass = NSBlock
iOS-block[18429:234959] block.class.superclass.superclass.superclass = NSObject
说明了上面代码中的block的类型是__NSGlobalBlock
,继承关系可以表示为__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
block有3种类型
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
NSGlobalBlock ( _NSConcreteGlobalBlock )
NSStackBlock ( _NSConcreteStackBlock )
NSMallocBlock ( _NSConcreteMallocBlock )
其中三种不同的类型和环境对应如下
| block类型 | 环境 |
| ------------ | ------------- | ------------- |
| NSGlobalBlock | 没有访问auto变量 |
| NSStackBlock | 访问了auto变量 |
|NSMallocBlock | NSStackBlock调用了copy |
其在内存中的分配如下对应
运行代码查看
MRC下
注意,以下代码在MRC下测试
注意,以下代码在MRC下测试
注意,以下代码在MRC下测试
因为ARC的时候,编译器做了很多的优化,往往看不到本质,
改为MRC方法: Build Settings
里面的Automatic Reference Counting改为NO
如下图所示
用代码来表示
void (^block)(void) = ^{
NSLog(@"123");
};
NSLog(@"没有访问auto block.class = %@",[block class]);
auto int a = 10;
void (^block1)(void) = ^{
NSLog(@"a = %d",a);
};
NSLog(@"访问了auto block1.class = %@",[block1 class]);
NSLog(@"访问量auto 并且copy block1-copy.class = %@",[[block1 class] copy]);
输出为
OS-block[23542:349513] 没有访问auto block.class = __NSGlobalBlock__
iOS-block[23542:349513] 访问了auto block1.class = __NSStackBlock__
iOS-block[23542:349513] 访问量auto 并且copy block1-copy.class = __NSStackBlock__
可以看出和上面说的
| block类型 | 环境 |
| ------------ | ------------- | ------------- |
| NSGlobalBlock | 没有访问auto变量 |
| NSStackBlock | 访问了auto变量 |
|NSMallocBlock | NSStackBlock调用了copy |
是一致的
ARC下
在ARC下,上面的代码输出结果为下面所示,因为编译器做了copy
iOS-block[24197:358752] 没有访问auto block.class = __NSGlobalBlock__
iOS-block[24197:358752] 访问了auto block1.class = __NSMallocBlock__
iOS-block[24197:358752] 访问量auto 并且copy block1-copy.class = __NSMallocBlock__
block的copy
前面说了在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,具体来说比如以下情况
copy的情况
1.block作为函数返回值时
2.将block赋值给__strong指针时
3.block作为Cocoa API中方法名含有usingBlock的方法参数时
4.block作为GCD API的方法参数时
5.block作为函数返回值时
block作为函数返回值时
// 定义Block
typedef void (^YZBlock)(void);
// 返回值为Block的函数
YZBlock myblock()
{
int a = 6;
return ^{
NSLog(@"--------- %d",a);
};
}
YZBlock Block = myblock();
Block();
NSLog(@" [Block class] = %@", [Block class]);
输出为
iOS-block[25857:385868] --------- 6
iOS-block[25857:385868] [Block class] = __NSMallocBlock__
上述代码如果再MRC下输出__NSStackBlock__
,在ARC下,自动copy,所以是__NSMallocBlock__
将block赋值给__strong
指针时
// 定义Block
typedef void (^YZBlock)(void);
int b = 20;
YZBlock Block2 = ^{
NSLog(@"abc %d",b);
};
NSLog(@" [Block2 class] = %@", [Block2 class]);
输出为
iOS-block[26072:389164] [Block2 class] = __NSMallocBlock__
上述代码如果再MRC下输出__NSStackBlock__
,在ARC下,自动copy,所以是__NSMallocBlock__
block作为Cocoa API中方法名含有usingBlock的方法参数时
eg:
NSArray *array = @[@1,@4,@5];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// code
}];
block作为GCD API的方法参数时
eg
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//code to be executed after a specified delay
});
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
block总结(二)