iOS开发中block随处可见,什么是block呢?block是一个匿名函数,是一个代码块,把代码放在这个代码块中,在需要使用的时候进行调用。block会封装函数以及函数的调用环境:
-封装函数:是指block会把block内部的参数返回值、执行体封装成一个函数,并且存储该函数的内存地址。
-封装函数的调用环境:是指block会捕获变量,并把这些变量存储起来。
将OC上层的代码还原成c++代码的方式:
- clang -rewrite-objc Input.m -o Output.cpp
- xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc Input.m -o Output.cpp
一、block的类型和使用
iOS
开发中有三种类型的block
,他们分别是全局block
,堆block
、栈block
。
全局block(__NSGlobalBlock__)
:没有访问外界局部普通变量的block
就是全局block
,存储在全局区。
堆block(__NSMallocBlock__)
:对栈block进行copy操作返回的block
就是堆block
,存储在堆区。
栈block(__NSStackBlock__)
:访问了外界普通局部变量的block
就是栈block
,存储在栈区。
注意:本章节的代码均在
MRC
环境下开启的调试,MRC
下新建的仅访问局部普通变量的block
是栈block
,进行copy
之后变成堆block
。ARC
下编译器会自动将创建的栈block转换成堆block
,如果不做转换可以在block
前添加__weak
关键字。例如void(^__weak testBlock)() = ^{}
这样定义即可。
测试代码
- (void)main{
int sValue = 20;
static int gValue = 20;
void(^gBlock)(void) = ^(){
NSLog(@"value=%d", gValue);
};
id cgBlock = [gBlock copy];
void(^sBlock)(void) = ^(){
NSLog(@"value=%d", sValue);
};
id csBlock = [sBlock copy];
NSLog(@"gBlock=%@,copy之后%@",gBlock,cgBlock);
NSLog(@"sBlock=%@,copy之后%@",sBlock,csBlock);
}
运行结果:
gBlock=<__NSGlobalBlock__: 0x107728490>,copy之后<__NSGlobalBlock__: 0x107728490>
sBlock=<__NSStackBlock__: 0x7ffee84e1b68>,copy之后<__NSMallocBlock__: 0x600002e20960>
通过如上代码,我们知道如果仅访问的局部静态变量,那么他仍然是个全局block
,全局block copy
之后仍然是个全局block
(仍指向原来的内存区域)。如果访问了局部普通变量,那么他就是栈block
,拷贝之后成了一个全新的堆block
(指向的内存区域地址发生了变化)。
在使用的过程中,包括三个部分block的声明(定义)、block的实现,block的调用:
block
的声明的完整写法:返回值类型
+ (
+ ^
+ 属性名称
+ )
+ 参数列表
:
//作为属性
@property (nonatomic, copy) int (^calcHashValue)(id value);
//作为函数参数:
- (void)getHashValue:(int(^)(id value)) calcHashValue
//可以采用别名的方式简化block的定义:
typeof int (^CalcHashValue)(id value);
//作为属性
@property (nonatomic, copy) CalcHashValue calcHashValue;
//作为函数参数:
- (void)getHashValue:(CalcHashValue)calcHashValue;
block的实现完整写法:^
+ 返回值类型
+ 参数列表
+ 函数体
+;
:
//返回值类型:没有的话可以不写,也可以写`void`;
//参数列表 :用小括号把参数列表包裹起来,参数之间用逗号分隔。没有参数的话可以写成`(void)`或者不写。
self.calcHashValue = ^int(id value){ return 22222222;};
//无参数,无返回值案例:
typedef void(^TodoValue)(void);
@property (nonatomic, copy) TodoValue todoValue;
self.todoValue = ^void(void){};或者self.todoValue = ^{};
block的调用:
//需要注意的是在不能保证block有实现的时候,一定要检查是否为空
if(self.calcHashValue) self.calcHashValue(self);
block在使用的时候,会有一些需要注意的问题:
- 1.block内部不能修改局部变量(直接报错:
Variable is not assignable (missing __block type specifier)
)。如果需要修改则需要在局部变量的定义时加上__block
关键字。
__block int localValue = 10;
void(^sBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);//localValue=10
localValue = 20;
};
sBlock();
NSLog(@"localValue=%d", localValue);//localValue=20
- 2.block实现之后调用之前修改局部变量,block中拿到的仍然是旧值。如果要获得最新值则需要在局部变量定义时加上
__block
关键字。
//不加block关键字
int localValue = 10;
void(^sBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue); //localValue=10
};
localValue = 20;
sBlock();
//加block关键字
__block int localValue = 10;
void(^sBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);//localValue=20
};
localValue = 20;
sBlock();
这个block关键字到底是做了什么呢?
二、block的底层实现
我们通过clang看下block底层是如何实现的
xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc NXBlock.m
其中原始代码如下:
@interface NXBlock : NSObject
- (void)t1;
- (void)t2;
@end
@implementation NXBlock
- (void)t1{
int localValue = 10;
void(^noneBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);
};
noneBlock();
}
- (void)t2{
__block int localValue = 10;
void(^withBlock)(void) = ^(){
NSLog(@"localValue=%d", localValue);
};
withBlock();
}
@end
生成.cpp文件后我们拷贝出相关的代码片段:
如下本段是localValue没有使用__block修饰的:
struct __NXBlock__t1_block_impl_0 {
struct __block_impl impl;
struct __NXBlock__t1_block_desc_0* Desc;
int localValue;
__NXBlock__t1_block_impl_0(void *fp, struct __NXBlock__t1_block_desc_0 *desc, int _localValue, int flags=0) : localValue(_localValue) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __NXBlock__t1_block_func_0(struct __NXBlock__t1_block_impl_0 *__cself) {
int localValue = __cself->localValue; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_0, localValue);
}
static struct __NXBlock__t1_block_desc_0 {
size_t reserved;
size_t Block_size;
} __NXBlock__t1_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t1_block_impl_0)};
static void _I_NXBlock_t1(NXBlock * self, SEL _cmd) {
int localValue = 10;
void(*noneBlock)(void) = ((void (*)())&__NXBlock__t1_block_impl_0((void *)__NXBlock__t1_block_func_0, &__NXBlock__t1_block_desc_0_DATA, localValue));
((void (*)(__block_impl *))((__block_impl *)noneBlock)->FuncPtr)((__block_impl *)noneBlock);
}
如下本段是localValue使用__block修饰的:
struct __Block_byref_localValue_0 {
void *__isa;
__Block_byref_localValue_0 *__forwarding;
int __flags;
int __size;
int localValue;
};
struct __NXBlock__t2_block_impl_0 {
struct __block_impl impl;
struct __NXBlock__t2_block_desc_0* Desc;
__Block_byref_localValue_0 *localValue; // by ref
__NXBlock__t2_block_impl_0(void *fp, struct __NXBlock__t2_block_desc_0 *desc, __Block_byref_localValue_0 *_localValue, int flags=0) : localValue(_localValue->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __NXBlock__t2_block_func_0(struct __NXBlock__t2_block_impl_0 *__cself) {
__Block_byref_localValue_0 *localValue = __cself->localValue; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_fz_f2dd5f4545ggwp26d9pyjfwc0000gn_T_NXBlock_7c795f_mi_1, (localValue->__forwarding->localValue));
}
static void __NXBlock__t2_block_copy_0(struct __NXBlock__t2_block_impl_0*dst, struct __NXBlock__t2_block_impl_0*src) {
_Block_object_assign((void*)&dst->localValue, (void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __NXBlock__t2_block_dispose_0(struct __NXBlock__t2_block_impl_0*src) {
_Block_object_dispose((void*)src->localValue, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static struct __NXBlock__t2_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __NXBlock__t2_block_impl_0*, struct __NXBlock__t2_block_impl_0*);
void (*dispose)(struct __NXBlock__t2_block_impl_0*);
} __NXBlock__t2_block_desc_0_DATA = { 0, sizeof(struct __NXBlock__t2_block_impl_0), __NXBlock__t2_block_copy_0, __NXBlock__t2_block_dispose_0};
static void _I_NXBlock_t2(NXBlock * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_localValue_0 localValue = {(void*)0,(__Block_byref_localValue_0 *)&localValue, 0, sizeof(__Block_byref_localValue_0), 10};
void(*withBlock)(void) = ((void (*)())&__NXBlock__t2_block_impl_0((void *)__NXBlock__t2_block_func_0, &__NXBlock__t2_block_desc_0_DATA, (__Block_byref_localValue_0 *)&localValue, 570425344));
((void (*)(__block_impl *))((__block_impl *)withBlock)->FuncPtr)((__block_impl *)withBlock);
}
我们通过一个表格来对比两者的异同:
对比 | 无修饰 | __block修饰 | static修饰 | __weak修饰 |
---|---|---|---|---|
定义 | int localValue = 10; |
__block int localValue = 10; |
static int localValue = 10; |
__weak id self = self; |
完整结构与构造函数 |
struct __NXBlock__t1_block_impl_0{} :-1.包括impl、Desc和localValue三个变量。 2.其中localValue这个外部局部变量在内部的定义仍然是int localValue。 3.构造函数包括调用的函数指针+描述+int型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags。 |
struct __NXBlock__t2_block_impl_0{} :1.包括impl、Desc和localValue三个变量。 2.其中localValue这个外部局部变量在内部的定义为__Block_byref_localValue_0 *localValue;是一个结构体指针; 3.构造函数包括调用的函数指针+描述+__Block_byref_localValue_0型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags。 |
struct __NXBlock__t3_block_impl_0{} ;1.包括impl、Desc和localValue三个变量。 2.其中localValue这个外部变量在内部的定义是int *localValue。 3.构造函数包括调用的函数指针+描述+int*类型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags。 |
struct __NXBlock__t4_block_impl_0{} ;1.包括impl、Desc和localValue三个变量。 2.其中localValue这个外部变量在内部的定义是__weak id localValue。 3.构造函数包括调用的函数指针+描述+weak id类型参数_localValue+flags,分别赋值给impl.FuncPtr、Desc、localValue和impl.Flags |
外部变量在内部的定义 | struct->localValue访问原始数据; | 封装在struct __Block_byref_localValue_0{}结构体中 1. 整形int类型变量localValue存储了外部传入的localValue值. 2.结构体指针类型__Block_byref_localValue_0 *的变量指向自己(localValue(_localValue->__forwarding)说明了这一点)。 3.struct->localValue->__forwarding->localValue即是原始localValue; |
(*struct.localValue)访问原始数据。 | struct.localValue访问原始数据 |
block函数指针 | __NXBlock__t1_block_func_0(...)即是外部调用block的执行函数。 1.函数包括一个完整block的参数__cself。 2.通过__cself->localValue获得外界的值。 |
__NXBlock__t2_block_func_0(...)即是外部调用block的执行函数。 1.函数包括一个完整block的参数__cself。 2.通过__cself->localValue->__forwarding->localValue获得外界的值。 |
__NXBlock__t3_block_func_0(...)即是外部调用block的执行函数。 1.函数包括完整block的参数__cself。 2.通过(*__cself.localvalue)获得外界的值。 |
__NXBlock__t4_block_func_0(...)即是外部调用block的执行函数。 1.函数包括完整block的参数__cself。 2.通过__cself.localvalue获得外界的值 |
描述结构 | __NXBlock__t1_block_desc_0: 1:保留字段reserved默认值为0; 2.Block_size记录完成结构体的大小; |
__NXBlock__t2_block_desc_0; 1.保留字段reserved默认值为0。 2.Block_size记录完成结构体的大小; 3.copy函数指针; 4.dispose函数指针; |
__NXBlock__t3_block_desc_0: 1:保留字段reserved默认值为0; 2.Block_size记录完成结构体的大小; |
__NXBlock__t4_block_desc_0: 1:保留字段reserved默认值为0; 2.Block_size记录完成结构体的大小; |
copy函数 | / | __NXBlock__t2_block_copy_0; | / | / |
dispose函数 | / | __NXBlock__t2_block_dispose_0; | / | / |
-
__Block_byref_localValue_0
构造过程中传给__forwarding
的是__Block_byref_localValue_0
类型,是变量自身,也就是src.__forwarding
指向的是自己,这个是block
在栈区的情况。 - 当栈区
block
被复制到堆区的时候,src
结构体会被复制一份,复制出来的在堆区copy. __forwarding = copy;
并且栈区src. __forwarding = copy;
了。 - 这样以来,无论是通过访问原有的栈区的
block
还是新拷贝的堆block
,那么通过block. byref. __forwarding
获取到的都是堆内存中的那一份。
我们可以通过一段代码验证这个结果:
__block int localValue = 10;
NSString *(^stackBlock)(void) = ^NSString *(){
return [NSString stringWithFormat:@"&localValue=%p", &localValue];
};
NSLog(@"拷贝前:%@-%@", stackBlock, stackBlock());
NSString *(^mallocBlock)(void) = [stackBlock copy];
NSLog(@"拷贝后:%@-%@", stackBlock, stackBlock());
NSLog(@"拷贝后:%@-%@", mallocBlock, mallocBlock());
打印结果
拷贝前:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x7ffee17f6ba8
拷贝后:<__NSStackBlock__: 0x7ffee17f6b60>-&localValue=0x600000472878
拷贝后:<__NSMallocBlock__: 0x600000a5d200>-&localValue=0x600000472878
可以看到拷贝后,新生成了一个堆block
对象并且block
捕获的localValue
的地址发生了变化,由0x7ffee17f6ba8
变成了0x600000472878
,而且原有的栈block
捕获的localValue
的地址也由0x7ffee17f6ba8
变成了0x600000472878
,跟堆block
保持一致。
细节逻辑可以在源码中看到:
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
简单总结:
1.没有使用__block
关键字修饰的localValue
是一个简单int
类型,传入内部也是的int
类型的localValue
变量.
2.使用__block
关键字修饰的localValue
会生成为__Block_byref
的结构体,传入内部也是__Block_byref
类型的localValue
结构体指针。
3.栈上的block
进行copy
:block
本身会在堆上开辟内存;__Block_byref
在堆上新开辟内存;捕获的外部变量也会在堆上新开辟内存。原有的栈block
的__Block_byref
的__forwarding
会指向堆block
的__Block_byref
。
三、block中的循环引用
如果对象A强持有对象B:
-初始化完成后A的引用计数为1,B的引用计数为1。B的引用计数会因为A的持有而+1变成2。
-在AB出作用域后系统会给AB分别发送一个release消息,A的引用计数-1变成0;B的引用计数-1变成1。
-A引用计数变成0调用dealloc方法进行销毁,A调用dealloc方法时也会给B发送一个release消息,B的引用计数-1变成0,则B会调用dealloc方法进行销毁。
如果对象A强持有对象B,B也强持有A:
-初始化完成后AB引用计数为1,相互赋值后两者的引用计数都变成2。
-AB出作用域后系统分别给AB发送release消息。A的引用计数变成1,B的引用计数变成1。
-两者维持引用计数为1而得不到释放,造成内存泄漏。
而block中的循环引用出现的通常是由于self强制有block,而block又强持有self造成的循环引用。如果能通过weak断开引用环那么问题就解决了。
2.1.使用weak(/strong)修饰断开环的方式避免循环引用:
准备一段代码:
@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(void);
@property (nonatomic, copy) void(^test)(void);
@end
@implementation NXTester
- (void)dealloc{
NSLog(@"NXTester-dealloc");
}
@end
案例1:如下代码会造成循环引用,tester持有work,work持有tester,形成环构成循环引用。解决方案看案例2.
NXTester *tester = [[NXTester alloc] init];
tester.work = ^{
tester;
};
tester.work();
案例2:如下代码不会造成循环引用,tester持有work,work持有weakself, weakself弱持有tester。弱引用断开了这个闭环,不会循环引用。这一点原因可以参考上述表格的__weak修饰的局部变量部分,在内部定义成__weak NXTester *类型。
NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
weakself;
};
tester.work();
案例3:如下代码会造成循环引用,注释掉的部分也会循环引用。因为strongself强持有test,test又强持有strongself,形成环构成循环引用,解决办法参考案例4.
NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
//weakself.test = ^{
// weakself;
//};
//weakself.test();
//或者
__strong NXTester *strongself = weakself;
strongself.test = ^{
strongself;
};
strongself.test();
};
tester.work();
案例4:如下代码不会造成循环引用。通过weak断开了引用环。
NXTester *tester = [[NXTester alloc] init];
__weak NXTester *weakself = tester;
tester.work = ^{
__strong NXTester *strongself = weakself;
__weak NXTester *weakweakself = strongself;
strongself.test = ^{
weakweakself;
};
strongself.test();
};
tester.work();
如上我们看到第一种解决循环引用的方式是weak-stong修饰。block内部需要访问的变量用在block外先用weak修饰,内部使用时在用strong修饰。
2.2、使用参数传递来解决循环引用的问题:
以上案例中,我们需要在block内部捕获外部的变量。我们也可以通过参数传递的方法来解决。
@interface NXTester: NSObject
@property (nonatomic, copy) void(^work)(NXTester *value);
@end
@implementation NXTester
- (void)dealloc{
NSLog(@"NXTester-dealloc");
}
@end
使用:
NXTester *tester = [[NXTester alloc] init];
tester.work = ^(NXTester *value){
value;
};
tester.work(tester);
2.3.使用完毕后将对象设置为nil
如果一个通过block执行任务后不再需要保留了,那么可以在block中将对象设置为nil,或在不使用之后把block或者对象设置为nil都可以解决问题。这种方案不推荐。
注意事项:
有些场景下不一定会造成循环引用,关键看有没有无法断开的引用环。