1.block
首先,我们了解下闭包的概念,闭包就是能够读取其他函数内部变量的函数,在很多语言上都有类似的实现,而block可以说就是OC上对闭包的实现,block实际上就是把代码块指向函数指针,并且能截获变量。
block的使用代码如下:
^{
NSLog(@"Hello");
};
可以看到代码被^{}
包起来了,其实^{}
只是个语言标记,clang编译器会将其转换为block结构体。
2.内存结构
block目前最新的结构体定义如下:
struct Block_layout {
void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
可以看到block
也具有isa
指针,指向__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__
所对应的地址,flags
为对应的flag数值,有很多作用,invoke
函数指针指向相应的代码块,第一个参数为block
本身的指针。这里主要就是isa
,他代表该block所对应的类型。
__NSGlobalBlock__
:当Block中没有引用外部变量,或引用了全局变量,const 变量或static变量时为该类型,存放在data段里。
__NSMallocBlock__
:当Block引用了外部的OC对象,Block对象或用__block
修饰的变量时则为该类型,存放在堆里,进行copy
操作的话,会增加引用计数。
__NSStackBlock__
:当Block中使用了外部栈变量,则为该类型,存放在栈里,在ARC下,block赋值后会被__strong
修饰,然后走objc_retainBlock->_Block_copy->_Block_copy_interna
流程,被移到__NSMallocBlock__
里,只有__weak
声明的block才在栈里。
可以通过[block class]
来获取到该block的类型。
3.截获变量
理解Block的关键,在于理解Block是如何处理外部变量的,Block中会截取这些类型的外部变量:
1.全局/静态变量
对于全局/静态变量,Block会直接引用这类变量,不会copy。
static int a = 13;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Outside Block, static int a address is %p", &a);
^{
NSLog(@"Inside Block, static int a address is %p", &a);
}();
}
这里打印出来的地址都是同一个,全局变量因为作用域的问题,在block内直接访问修改是没问题的,且不会保存到block的结构体内,而静态变量因为内存地址不变,所以传递到block内用的是指针地址,所以也可以直接访问和修改。
2.自动(auto)存储类型/局部变量
对于auto
类型的变量,(Block类型、__block、NSObject类型除外),block
内部是使用const copy
了一份,并存放到block
结构体内比如下面:
int b = 12;
NSLog(@"Outside Block, address of int b is %p", &b);
^{
NSLog(@"Inside Block, address of int b is %p", &b);
}();
这里打印的是不同地址,block
内部结构是:
struct __block_literal_2 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_2 *);
struct __block_descriptor_2 *descriptor;
const int ; // 这里会有一份const copy
};
一般的,对于标量类型(int, float, bool等基本类型),struct,unions和函数指针类型,都会采用const copy
的方式,将Block
外部的变量拷贝到Block内部。
3.对象类型(Block/NSObject)
截取NSObject对象时,同样会做一份const copy,下面代码:
NSObject *obj = [NSObject new];
NSLog(@"Outside Block, address:%p-----instance:%@", &obj,obj);
^{
NSLog(@"Inside Block, address:%p-----instance:%@", &obj,obj);
}();
打印了:
Outside Block, address:0x16f445368-----instance:<NSObject: 0x283ec45a0>
Inside Block, address:0x16f445360-----instance:<NSObject: 0x283ec45a0>
说明block
只copy了指针,并没有copy对象,只是浅拷贝
。这里有个细节,如何是修改对象的话,会直接报错,当是修改对象内部的成员变量是没问题的,因为修改成员变量并不会影响对象本身的内存地址,而且因为是浅拷贝
,所以修改的值出了block也是生效的。比如下面这代码:
NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
blockType blk = ^{
[str appendString:@" World"];
};
blk();
NSLog(@"Now str is %@", str);
是可以正常打印Hello World
的,因为appendString
并不会改变str指针本身,只是修改了指针指向的内容。
4.__block修饰的变量
这里最复杂的就是__block
修饰的变量,除了全局变量和静态变量外,当我们想在block
内修改变量的值时,需要使用__block
修饰。看下如下代码:
__block int b = 13;
NSLog(@"Outside Block, address of __block int b is %p, b = %d", &b, b);
blockType1 blk = ^{
b++;
NSLog(@"Inside Block, address of __block int b is %p, b = %d", &b, b);
};
blk();
NSLog(@"After Block, address of __block int b is %p, b = %d", &b, b);
这里变量b
在进入block后,指针变了,这里实际上被转换成_block_byref_i
结构了,比如:
__block int i = 10;
i = 11;
使用clang改写变成了:
struct _block_byref_i {
void *isa;
struct _block_byref_i *forwarding;
int flags; //refcount;
int size;
int captured_i;
} i = { NULL, &i, 0, sizeof(struct _block_byref_i), 10 };
i.forwarding->captured_i = 11;
可以看到,__block int i
被改写为了struct _block_byref_i
结构体,这里关键属性就是forwarding
,forwarding
指向一个block结构体,当block在栈上时,forwarding
指向的是自身block,当block
copy到堆上时,forwarding
指向的是堆上的block地址,而堆上的block的forwarding
指向自身。如下图所示:
所以前面的变量b
才会出现指针地址被改变的问题。
4.用途
block
有以下几种用法:
1.作为属性
@property (nonatomic, copy) void(^block)(void);
这里block
使用了copy
来修饰,其实在ARC下,使用strong
和copy
效果是一样的,底层实现都是走_Block_copy
方法,创建后的block
是__NSMallocBlock__
类型的。
如果使用weak
修饰的话,创建后的block
是__NSStackBlock__
类型的,但是就有可能被栈释放,所以我们一般用strong
或者copy
。
另外如果该block
的实现里没有捕获外部变量或者捕获了全局变量,静态变量,那么类型会优先变成__NSGlobalBlock__
,不受修饰影响。
2.作为参数
- (void)testBlock:(void(^)(void))block {
}
//1.传入成员属性
[self testBlock:self.block];
//2.传入临时创建的block,并捕获外部变量
[self testBlock:^{
NSLog(@"%@",num);
}];
//3.传入临时创建的block,不捕获外部变量
[self testBlock:^{
}];
在上面的3中调用方式中,第一种传入成员属性,因为传的是指针地址,所以该block
等同于self.block
的内存分布。
第二种传入临时创建的block
,该block
因为捕获了外部变量,如果该变量非全局或静态变量,则该block
为__NSStackBlock__
类型。
第三种因为没有捕获任何外部变量,所以该block
为__NSGlobalBlock__
类型,生命周期等同于app,也就是会一直常驻内存。
3.作为返回值
//1.返回成员属性
- (void(^)(void))returnBlock {
return self.block;
}
//2.返回临时创建的block,并捕获外部变量
- (void(^)(void))returnBlock {
NSNumber *num = @0;
return ^{
NSLog(@"%@",num);
};
}
//3.返回传入临时创建的block,不捕获外部变量
- (void(^)(void))returnBlock {
NSNumber *num = @0;
return ^{
};
}
以上3种返回值,其block
的内存分布基本与作为参数时相关,唯一不同的是,第二种block
引用了外部变量,本来是__NSStackBlock__
类型的,但是因为需要return,会被__strong修饰,所以变成了__NSMallocBlock__
。
5.循环引用
当对象内部的成员变量引用了block,而block又引用了对象时,会导致循环引用。
下面几种情况都会造成循环引用:
这里self->self.strBlk->self.array->self
,是最常见的循环引用,能被leak和xcode检测出来,当是如果这里多嵌套几层self->self.ival->self.strBlk->self.array->self
,也会导致循环引用,但是leak会检测不出来,xcode也不会报警。
上面这种循环引用就比较难发现了,这里在MRC下是不会循环引用了,当是在ARC下会出现,因为self->self.blk->blockSelf->self
,主要是__block
修饰的self,实际是个block结构体,但是内部会持有self。
参考资料:
https://blog.csdn.net/u013378438/article/details/87167133
https://www.jianshu.com/p/ee9756f3d5f6
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
https://www.jianshu.com/p/fa76921131f9