在讨论Block截获对象的内存变化前。先看一下Block截获对象时,截获的是什么。
下面举个例子
// main.m
#import <Foundation/Foundation.h>
typedef void(^blk)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [[NSMutableArray alloc] init];
blk block = ^{
NSLog(@"%@", array) // 内部截获了array对象
};
block();
}
return 0;
}
这样看不够直观,通过clang将Objective-C代码转换为C++源码。
步骤如下:
1.找到对应的文件所在
2.在终端中输入
clang -rewrite-objc main.m
3.得到main.cpp
找到关键点__main_block_impl_0。就是对应上述blk类型变量的对应结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *array; // 成员变量,用来保存外部传入的array指针值
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以明显得看到结构体内部含有成员变量,NSMutableArray *array,就是用来存储外部传入的array指针值。
由此可证最开始的猜想:Block截获对象时,保存的指向对象的指针值。
Block截获对象时的,对象的内存变量
确切来说,是对象引用计数的变化。
接着针对上面的例子,获得截获对象的引用计数。
// main.m
#import <Foundation/Foundation.h>
typedef void(^blk)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [[NSMutableArray alloc] init];
// 位置1
NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));
// 位置2
blk block = ^{
NSLog(@"%@", array);
};
// 位置3
NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));
}
return 0;
}
下面来看看输出结果
count:1
count:3
位置1时,count为1不难理解,此时指向对象的只有一个array指针。而过了位置2之后,count变成了3。
这里有两个疑惑点。
1.block没有执行的情况下,对象已经被截获了?
2.count为什么是3,而不是2?
一个一个来解答。
1.block没有执行的情况下,对象已经被截获了?
通过clang的源码转换如下。
// main.cpp
// 简化结果
int main(int argc, const char * argv[]) {
blk block = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344);
return 0;
}
可以看到将__main_block_impl_0赋值给了block变量,而__main_block_impl_0正是之前例子提到的Block变量转化后的结构体。结构体内部有一个成员变量专门用来存对象指针的值。因此只要定了Block,对象就已经被截获了。
2.count为什么是3,而不是2?
通过第一个解答,得知此时array对象已经被截获。那么引用计数+1。但为什么多了1。
原因是,在ARC的情况下,有以下4中情况,会将Block从栈空间复制到堆空间
- Block变量使用copy
- Block变量被__strong修饰符修饰
- 方法返回Block变量
- Cococa框架中带“usingBlock”的方法及GCD中的API
在ARC中,指向OC对象的变量没有明确写所有权修饰符,则修饰符默认为__strong。(实际上Block就是一种OC对象)
blk block = ^{
NSLog(@"%@", array);
};
等价于
blk __strong block = ^{
NSLog(@"%@", array);
};
根据规则2,“Block变量被__strong修饰符修饰”时,会将Block从栈空间复制到堆空间。
则此时内存中有两份Block,且两份Block又各自截获了array对象。因此此时指向对象的引用计数为3(array变量+栈Block截获+堆Block截获)。
那么,如何使count变为2呢?
思路就是只保留栈空间的Block。
// main.m
#import <Foundation/Foundation.h>
typedef void(^blk)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *array = [[NSMutableArray alloc] init];
NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));
blk __unsafe_unretained block = ^{
NSLog(@"%@", array);
}; // 使用__unsafe_unretained修饰
NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));
}
return 0;
}
结果
count:1
count:2
通过__unsafe_unretained进行修饰,即可只保留栈空间的Block。__unsafe_unretained作用于weak相似,但是不会自动置为nil。
备注:
不用weak的原因是因为Block刚生成对象,赋值的瞬间就被释放了。
网友评论