前言
在文章之前,先抛出如下问题。block的原理是怎样的?本质是什么?
`__block`的作用是什么?有什么使用注意点?
block的属性修饰词为什么是copy?使用block有哪些使用注意?
block一旦没有进行copy操作,就不会在堆上
block在修改NSMutableArray,需不需要添加__block?
如果现在不是很熟悉,希望看完这篇文章,能有个新的认识。
导读
本文主要从如下几个方面讲解blockblock的基本使用
block在内存中的布局
block对变量的捕获分析
MRC和ARC的对比
`__block`的分析
block中内存管理问题
block导致的循环引用问题
什么是blockIn 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.
翻译过来表达就是闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
block 实际上就是 Objective-C 语言对于闭包的实现。
block的基本使用block本质上也是一个OC对象,它内部也有个isa指针
block是封装了函数调用以及函数调用环境的OC对象
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);
}
用一幅图来表示
变量捕获
其实上面的代码我们已经看得出来变量捕获了,这里继续详细分析一下
局部变量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;
}
从cpp文件可以看出来,并没有捕获全局变量age1和height1,访问的时候,是直接去访问的,根本不需要捕获
小结auto修饰的局部变量,是值传递
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 )`
其中三种不同的类型和环境对应如下
其在内存中的分配如下对应
运行代码查看
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__
可以看出和上面说的
是一致的
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的情况block作为函数返回值时
将block赋值给__strong指针时
block作为Cocoa API中方法名含有usingBlock的方法参数时
block作为GCD API的方法参数时
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);