通过上一篇文章对block本质的分析,我们可以了解到,block的本质就是一个OC对象,拥有一个isa指针,那么block就肯定有自己的类型。上一篇文章中通过C++代码我们也看到了,isa指向一个&_NSConcreteStackBlock,本文就继续分享block的类型,继承链等知识点。
继承关系
block有三种类型,可以通过isa或者调用class方法获取,最终都继承自NSObject.
下面我们通过class获取到block的类型
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^myBlock)(void) = ^{
NSLog(@"this is a block ");
};
NSLog(@"%@",[myBlock class]);
NSLog(@"%@",[[myBlock class] superclass]);
NSLog(@"%@",[[[myBlock class] superclass] superclass]);
NSLog(@"%@",[[[[myBlock class] superclass] superclass] superclass]);
}
return 0;
}
结果如下,也更加印证了block是一个OC对象。
block[12750:370967] __NSGlobalBlock__
block[12750:370967] __NSGlobalBlock
block[12750:370967] NSBlock
block[12750:370967] NSObject
block类型
上面我们分析出了block都继承自NSBlock,最终继承自NSObject,下面继续看看block的三种类型
- NSGlobalBlock
- NSMallocBlock
- NSStackBlock
还是通过class的方式获取block的类型
//void (^block1)(void) 起个别名
void (^block1)(void) = ^{
NSLog(@"this is a block");
};//全局 静态 :__NSGlobalBlock__ 0x1开头
int age = 10;
void (^block2)(void) = ^{
NSLog(@"this is a block %d",age);
};//__NSMallocBlock__ 堆block 0x6开头
NSLog(@"%@ --- %@ --- %@",[block1 class],[block2 class],[^{
NSLog(@"this is a block %d",age); //__NSStackBlock__ 栈block 0x7开头
} class]);
输出:
__NSGlobalBlock__ --- __NSMallocBlock__ --- __NSStackBlock__
但是我们还是需要编辑成C++代码再看一下isa指向的类型,就不贴代码了,直接说结果,都是_NSConcreteStackBlock;
这里要说一下一切以运行时的结果类型为准,因为clang这个命令编译的结果可能跟我们最初写的代码有些出入,只能作为学习的参考。
那么上面的三个block为什么是这三种类型呢,下面我们继续从内存分布上去分析。
内存分布
一图胜前言

-
编写的代码都放在程序区域
-
全局变量一般放在数据区域
-
程序区域和数据区域的内存, 都是编译器自动处理的
-
堆:动态分配内存 比如
[NSObject alloc],malloc(20)需要开发者申请内存也需要自己管理内存 -
栈:局部变量,函数参数等,系统自动分配内存,自动释放内存
-
NSGlobalBlock就放在数据区域,NSMallocBlock放在堆区 ,NSStackBlock放在栈区
那么我们开发中写的很多block,我们怎么知道他是什么类型的block呢?
下面总结几点判断的规则:
- NSGlobalBlock:没有访问auto变量就是NSGlobalBlock,对NSGlobalBlock进行copy操作依然是NSGlobalBlock
- NSStackBlock:访问了auto变量就是NSStackBlock(需要在MRC环境测试,因为ARC环境编译器在打印的时候帮我们做了copy操作)
- NSMallocBlock:NSStackBlock做了copy操作就是NSMallocBlock
为什么上面要强调一下
copy操作呢,因为在栈上的block调用copy之后会从栈复制一份到堆上,在堆上的block再copy,则会引用计数加1,而在数据区的NSGlobalBlock调用copy则不会有任何操作。
block的copy
上面内存分部中说过NSStackBlock做了copy操作就是NSMallocBlock,在ARC下,系统会根据情况自动把栈上的block copy到堆上,那是什么情况系统会做这件事呢?下面举例说一说
- block作为函数的返回值
typedef void (^MyBlock)(void);
MyBlock returnBlock() {
int age = 20;
//自动调用copy
return ^{
NSLog(@"----- %d",age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyBlock block = returnBlock();
block();
NSLog(@"%@",[block class]);//NSMallocBlock
}
return 0;
}
根据我们上面分析的结果,调用了auto变量,是一个NSStackBlock,但是最后打印的结果是一个NSMallocBlock,说明编译器帮我们做了一次copy操作,当然在ARC下我们也不需要关心release,在作用域结束,编译器也帮我们做了release操作。
- block赋值给__strong指针
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
MyBlock block = ^{
NSLog(@"----- %d",age);
};
block();
NSLog(@"%@",[^{
NSLog(@"----- %d",age);
} class]);//NSStackBlock
NSLog(@"%@",[block class]); //NSMallocBlock
}
return 0;
}
- API中方法名含有usingBlock的方法
NSArray *array = [NSArray array];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
- GCD中的block
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
<#code to be executed once#>
});
总结一下:通过表面看本质,栈区的block是不会对外面的变量执行retain操作的(MRC),或者说不会对外面变量发生强引用(ARC),只有堆区的block才有可能持有变量。
__block修饰符
说到__block这个修饰符,大家开发中一定是用过很多次了,咱们先从为什么要用开始说起,再说__block加了这个修饰词之后到底发生了什么。
为什么要用__block呢?最常见的一个使用就是在block里面想修改block外部的局部变量,前面我们也已经分析过了,auto变量是通过值传递,被记录在block的结构体中的,而且是跨函数访问,我们如何能在block执行的函数中,修改另外一个函数中的auto变量呢,当然是改不了的。然后我们又知道了只要用__block修饰这个想改的auto变量,我们就可以在block中修改他的值了,那么这里就可以想一想__block这个修饰词到底是做了什么呢?也许他就是把之前的值传递改成了地址传递呢?
修改auto变量
想要修改auto变量,我们先说一种不是使用__block的方法
static修饰前面我们也分析过static修饰的变量被block捕获是地址传递,只要是地址传递我们能拿到地址,就可以根据地址修改对应内存中的内容,可以肯定,static修饰,是可以修改auto变量的全局变量就更不用说了,谁都可以改,都不用通过block去捕获__block除了上面两种方法(可能会有内存问题,我们一般不用),__block才是我们最优的一种解决方法
__block原理
还是通过clang编译成C++代码区分析__block的原理,先写一段测试代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int age = 20;
MyBlock block = ^{
age = 33;
NSLog(@"----- %d",age);
};
block();
}
return 0;
}
__block int age = 20;这句代码最终被编译成了
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 20};
其实这里可以理解为,添加了__block修饰之后的age,被编译成了一个__Block_byref_age_0对象
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
其中__forwarding被赋值了&age,也就是__Block_byref_age_0对象本身的地址,age的值(20)也被存储在了这个对象中。然后就是block中修改age的值,并打印age,使用age->__forwarding->age取到age的值
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) = 33;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_ljphgvys0hzg8hfxdtv5q5gm0000gn_T_main_76267f_mi_0,(age->__forwarding->age));
}
到这里我们应该已经清楚了,为什么__block修饰的变量在block中可以被修改,其实说白了,还是变相的实现了地址的传递,只有拿到地址才可以改变相应地址指向的内存区域中的数据。
__forwarding
这里为什么要把__forwarding那出来说一说呢,肯定有人在好奇,(age->__forwarding->age) = 33;这个取值方式,还有结构体里面为什么要有一个__forwarding,__forwarding不就是指向自己吗,到底有什么用非要这样设计?
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 20};
这里可以看到,给__forwarding赋值了&age,就是他自己的内存地址,然而这里__forwarding指针真的永远指向自己么?我们来做一个实验。
//以下代码在MRC中运行
__block int i = 0;
NSLog(@"Block 外面 %p",&i);
void (^myBlock)(void) = [^{
i ++;
NSLog(@"Block 里面 %p",&i);
}copy];
把Block拷贝到了堆上,这个时候打印出来的2个i变量的地址就不同了。
Block 外面 0x7fff5fbff818
Block 里面 0x1002038a8
地址不同就可以很明显的说明__forwarding指针并没有指向之前的自己了。那__forwarding指针现在指向到哪里了呢?下面看图说话吧

这样应该就能明白__forwarding的存在就是为了,当block从栈上copy到堆上之后,还能继续指向堆上的__block变量的。
文字最后再说一个block中使用array的小问题,也是一个容易理解上混淆的问题:

addObject编译不会报错,但是给array赋值成nil就报错,这是为什么呢?
先说赋值nil报错,根据上面的分析,因为我们并没有用__block修饰array,block中不能修改auto对象,所以报错了。
再说addObject为什么不会报错,因为addObject其实并不是去改变array的值,只是去操作array,而block只是不能修改auto变量,并不是不能操作变量,所以不会报错。