block是什么?
1. block本质是一个OC对象,内部也有isa指针
2. block是封装了函数调用以及函数调用环境的OC对象 (block内部用到block外部对象也会封装到block内部)
a).block本质探究
在main.m中写一个block,并在arm64下编译成c++代码,方便查看实现如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
int num = 3;
void (^block2)(int) = ^(int a){
NSLog(@"用变量保存起来的 block, a = %d, num = %d", a, num);
};
block2(222);
}
return 0;
}
利用clang编译器把objc代码转成c++代码,终端命令如下:
//在main.n同目录下生成main.cpp的文件
/xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
查看编译后的main,cpp文件,可以看到main函数中block相关代码如下:
//声明block,删除强制转化代码,即
//声明
void (*block2)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
//调用
void (*block2)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
//以上代码删除强制转换修饰部分,如下
//__main_block_impl_0函数返回的地址即是block,函数的参数分别为__main_block_func_0和__main_block_desc_0_DATA以及num
void (*block2)(int) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA, num);
//调用block部分即 block2->FuncPtr(block, 222) ,即block2的FuncPtr函数指针调用
((void (*)(__block_impl *, int))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2, 222);
其中的block是名称为__main_block_impl_0的结构体,文件搜索可以看出结构体如下
//__block_impl结构体
struct __block_impl {
void *isa; //isa指针
int Flags;
int Reserved; //保留字段
void *FuncPtr; //存储的函数指针,指向block内部的实现部分
};
//__main_block_desc_0结构体
static struct __main_block_desc_0 {
size_t reserved; //保留字段
size_t Block_size; //block大小
}
//__main_block_impl_0结构体
struct __main_block_impl_0 {
struct __block_impl impl; //__block_impl结构体(见上面)
struct __main_block_desc_0* Desc; //__main_block_desc_0结构体指针
int num; //block对象内部存在外部的变量封装
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
忽略__main_block_impl_0结构体中c++部分构造函数__main_block_impl_0,实际上main()函数中的block结构如下
struct __main_block_impl_0 {
struct __block_impl impl; //__block_impl结构体(见上面)
struct __main_block_desc_0* Desc; //__main_block_desc_0结构体指针
int num; //block对象内部存在外部的变量封装
};
b).测试block如何捕获局部变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num_auto = 10;
//static 实际传递的是地址,若对象用static修饰,则是**
static int num_static = 10;
void (^block)(void) = ^(){
//num的值捕获进来了
NSLog(@"block内部打印,num_auto =%d, num_static=%d", num_auto, num_static);
};
num_auto = 20;
num_static = 20;
block();
}
return 0;
}
上述block执行完毕后,输出结果为10, 20。对于上述代码编译成成cpp文件如下
b).测试block如何捕获全局变量
int num1 = 10;
static int num2 = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^(){
//num的值捕获进来了
NSLog(@"block内部打印,num1 =%d, num2=%d", num1, num2);
};
num1 = 20, num2 = 20;
block();
}
return 0;
}
c).测试block如何捕获self
相关测试代码如下,
@interface Person : NSObject
@property (copy, nonatomic) NSString *name;
- (void)test;
@end
@implementation Person
- (void)test{
void(^block)(void) = ^(){
NSLog(@"------%@", _name);
};
block();
}
@end
编译成cpp代码分析
block内部对于当前成员变量的访问,其实也是对于当前block实例捕获后,再通过当前对象访问对应的成员变量或者属性的。
block捕获变量总结
变量类型 | 是否捕获到block内部 | 访问方式 | |
---|---|---|---|
局部变量 | auto | 是 | 值传递 |
static | 是 | 地址传递 | |
全局变量 | 否 | 直接访问 |
d).block类型分析
测试代码如下
void blockSuperclass() {
void(^block)(void) = ^(){ };
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
};
//通过以上代码分析继承关系,log结果为:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
//block可能类型测试代码,打印输出
void blockType() {
void(^block1)(void) = ^(){};
int num = 10;
void(^block2)(void) = ^(){
NSLog(@"num=%d", num);
};
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
NSLog(@"%d", num);
} class]);
}
//分析以上代码方法调用后的log输出结果为__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
//如果用clang编译成cpp文件,则对应类型如下 //对象clang中的_NSConcreteGlobalBlock _NSConcreteMallocBlock _NSConcreteStackBlock
//故可以看出block共有3中类型存在
//*切换MRC环境*,具体分析代码如下
int num = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int num1 = 10;
int num2 = 10;
void (^block1)(void) = ^{
NSLog(@"block1 没有访问auto变量--------");
};
void(^block2)(void) = ^{
NSLog(@"block3 访问了全局变量-------%d", num);
};
void (^block3)(void) = ^{
NSLog(@"block3 访问了static变量--------%d", num1);
};
void(^block4)(void) = ^{
NSLog(@"block4 访问了auto变量-------%d", num2);
};
NSLog(@"%@ %@ %@ %@", [block1 class], [block2 class], [block3 class], [block4 class]);
//block没有访问外部变量,一般用的较少
//MRC下输出结果:__NSGlobalBlock__ __NSGlobalBlock__ __NSGlobalBlock__ __NSStackBlock__
//ARC下输出结果:__NSGlobalBlock__ __NSGlobalBlock__ __NSGlobalBlock__ __NSMallocBlock__
}
return 0;
}
//捕获了auto变量的在栈上内存的__NSStackBlock__,若再次访问捕获的变量,此时变量可能被释放了,所以可能引起异常
//注意static类型的block调用了copy后,结果仍然是global类型
总结如下:
应用程序的内存分配 | block存储位置示例(从上到下,内存地址递增) | 存储内容 |
---|---|---|
程序区域.text区 | 程序代码段 | |
数据区域.data区 |
NSGlobalBlock (_NSConcreteGlobalBlock) |
全局变量存储区域 |
堆 |
NSMallocBlock (_NSConcreteMallocBlock) |
程序员手动开辟与释放 |
栈 |
NSStackBlock (_NSConcreteStackBlock) |
垃圾回收机制自动管理 |
类型 | 原存储位置 | 新存储位置 |
---|---|---|
NSGlobalBlock _ |
程序的数据区域 | 什么也不做 |
NSStackBlock _ |
栈 | 从栈复制到堆 |
NSMallocBlock _ |
堆 | 仅仅引用计数增1 |
关于block进行copy操作的验证代码
typedef void(^Block)(void);
Block getBlock1() {
//MRC下 返回的block是局部变量,栈上的block再次调用会很危险,此时需要手动进行copy操作
return ^(){
NSLog(@"无参数 无返回值的block");
};
}
Block getBlock2() {
int num = 2;
return ^(){
NSLog(@"无参数 无返回值的block, %d", num);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int num = 10;
//ARC下此时拿到的block已经在堆区间了, 此时执行完毕后不用关心释放,系统也会帮你自动释放
Block block = getBlock1();
//没有引用auto修饰的变量,则默认为__NSGlobalBlock__
NSLog(@"%@", [block class]);
//有强指针指向,此时block由__NSStackBlock__,变为__NSMallocBlock__
Block block2 = getBlock2();
NSLog(@"getBlock2----%@", [block2 class]);
//有访问auto变量,默认为__NSStackBlock__,又没有强指针__strong指向,所以不会copy到堆上,还是__NSStackBlock__
NSLog(@"%@", [^{
NSLog(@"----------%d", num);
} class]);
}
return 0;
}
总结就是,在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
1.block作为函数返回值时
2.将block赋值给__strong指针时
3.block作为Cocoa APi中方法名含有usingBlock的方法参数时
4.block作为GCD APi的方法参数时
e).对象类型auto变量与block的关系
默认情况下,方法调用完毕后,方法内部的对象释放,测试代码
typedef void(^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
{
Person *per = [[Person alloc] init];
per.age = 10;
}
Block block;
{
Person *per = [[Person alloc] init];
per.age = 20;
block = ^{
NSLog(@"---此时没有立即释放,因为ARC下此时block在堆空间了,block又引用了per.所以per没有释放---%d", per.age);
};
}
NSLog(@"对象类型的auto变量测试 log打印顺序");
}
}
若对于上述的Person对象per进行弱引用修饰 的话,
__weak Person *weakPerson = per;
block = ^{
NSLog(@"---此时没有立即释放,因为ARC下此时block在堆空间了,block又引用了per.所以per没有释放---%d", weakPerson.age);
};
//此时per对象会立即进行释放
上述Person对象会立即进行释放,执行如下命令,编译成cpp文件查看
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m
上述的block内部成员Person对象变成了弱引用,所以Person可以正常释放. 查看编译成的cpp代码
相关总结如下:
当block内部访问了对象类型的auto变量时:
如果block是在栈上,那么将不会对auto变量产生强引用,
若在堆上,则根据对象的修饰符(__strong、__weak、__unsafe_unretained)判断释放为强引用还是弱引用