本文正在参加「金石计划」
前阵子看到一条关于Block的问题如下
下面代码执行,控制台输出结果是什么?
NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
void(^block1)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block1();
void(^__weak block2)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block2();
void(^block3)(void) = [block2 copy];
block3();
__block NSObject *obj = [NSObject new];
void(^block4)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
};
block4();
在脑海里运算了一遍,大概得出来1 3 4 5 3的答案,然后发现选项居然没有我答案=_=,复制到Xcode运行一遍,最后一输出居然是1??那一刻感觉自己的蜜汁自信被打击到了,最后一个多了__block修饰究竟和第一个没有__block修饰有什么不一样?我自己知道的只有__block修饰的objc能在block里面读写,但是没有__block修饰只是能读取,私以为引用计数的机制都是一样的,找了很多资料,基本都没找到相关解释,终于在一篇有点擦边的文章看到这么一段话:
看到这句话时,我得到了一些启发,得出了最后为什么输出的1,如果我的想法不正确,欢迎大家指正我^ _ ^
首先我完整给一下这个题目的解答过程
第一个输出1:
NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
obj初始化,引用计数 +1,所以输出1。
其他相关知识点:
-
第二行代码中的CFGetRetainCount()函数是用来来打印类的实例对象的引用计数的函数,在文档中的说明如下:
public func CFGetRetainCount(_ cf: CFTypeRef!) -> CFIndex因为此函数接收的对象是Core Foundation中的对象/C指针(后面简称CF对象),而objc是Objective-C对象/OC指针(后面简称OC对象),所以要把OC对象转换成CF对象才能使用CFGetRetainCount()函数,在ARC下OC对象转换成CF对象的转换方式是用桥接,即__bridge,有如下3种:(__bridge type)expression (__bridge_retained CF type)expression (__bridge_transfer Objective-C type)expression它们有什么区别呢?
要知道在ARC模式下,ARC能管理OC对象,但不支持管理CF对象,所以转换后是谁来释放CF对象?上面3个方法均已经将该问题解决好,看开发者的实际方式进行使用
a.__bridge 不改变对象所有权, 看你是转换成OC还是CF对象,如果转换成CF对象,需要我们自己来管理内存, 它也是我们经常使用的方法, 从某种程度上来说, 它是下面两个方法的简化版本 b.__bridge_retained 等同于 CFBridgingRetain(),即将OC对象转换成CF对象,并把对象拥有权交给CF对象,剥夺ARC管理权,这个需要开发者后期用CFRelease或其他方法手动释放CF对象 c.__bridge_transfer 等同于 CFBridgingRelease().即将非OC对象转换成OC对象,同时管理权交给ARC,开发者无需手动管理内存
2.另外需要注意的是,swift中使用CFGetRetainCount的时候会把要打印对象的引用计数先加1,而swift的类初始化 的时候,引用计数默认就设置为1,所以swift打印的结果是2
第二个输出3:
void(^block1)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block1();
block1外部捕获了objc,进行了copy操作(将变量指针objc复制到block的数据结构中),所以objc的引用计数+1,又在ARC下会自动拷贝到堆,变成堆block,会进行强持有,拷贝时堆上的对象引用计数再次+1,就变成了3。
其他相关知识点:
1.ARC下栈block回自动拷贝到堆,变成堆block
2.block分为全局block/NSGlobalBlock,栈block/NSStackBlock,堆block/NSMallocBlock,可以用 NSLog(@“%@“,block); 来看这个block是什么类型的block,它们 的区别如下:
a.全局block,没有使用外部变量,或者只使用全局变量或静态变量,保存在数据段区,例如:
```
void(^block)(void) = ^{
NSLog(@"objc1");
};
block();
```
b.栈block,访问了自动局部变量,保存在栈区,MRC下写法如下:
int a = 1;
void(^block)(void) = = ^{
NSLog(@"%d",a);
};
block();
ARC下写法如下:(不加__weak,block会被copy到堆)
int a = 1;
void(^__weak block)(void) = ^{
NSLog(@"%d",a);
};
block();
c.堆block,栈块调用了copy,堆块是带引用计数的对象,保存在堆区,MRC下如果不再使用需要手动release释放,写法如下:
void(^block)(void) = [^{ NSLog(@"%d",a);} copy];
block();
ARC下写法如下:
int a = 1;
void(^block)(void) = ^{
NSLog(@"%d",a);
};
block();
第三个输出4:
void(^__weak block2)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block2();
block2被__weak修饰,看上面的延伸知识点得知这是一个栈block,所以obj只会被栈block捕获,引用计数+1,不会copy到堆,打印为4
第四个输出5:
void(^block3)(void) = [block2 copy];
block3();
block2被__weak修饰,看上面的延伸知识点得知这是一个栈block,block3对block2进行copy,又从上面的知识点得知这段代码相当于于把block2 copy到堆,然后值赋给block3,所以block3是一个堆block,obj引用计数再+1,打印为5
第五个输出1:
__block NSObject *obj = [NSObject new];
void(^block4)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));
};
block4();
重点来了,为什么用__block修饰的obj引用计数是1呢?这时候又回到第一张图了,里面提到:
对于用 __block 修饰的变量指针,即使一开始改变量指针的地址是A,在被block调用后,地址会如同block里面的一样被更改成C,如果这是真的话,那么引用计数器是1就可以解释了,因为A地址已经被回收了,objc在block调用后地址变成了C,block内部和外部都是同一个objc,现在进行验证一下
根据上面我之前提到的block在ARC下会先生成栈block,然后被拷贝到堆block,我用代码来模拟这个过程,先试试没有__block修饰的整个实现过程:
由上图可以看到:
objc1一开始的地址是0x7ff7b89e4b18,指向的值的内存地址是0x600001d3c3b0,然后进入到栈block1里,被栈block1捕获,自动生成一个新的objc1,其地址是0x7ff7b89e4b08,指向的值的内存地址仍然是0x600001d3c3b0,然后栈block1被block2执行copy(模拟ARC下先生成栈block,再copy到堆的操作),所以堆block2内部又复制出一个新objc1,其地址是0x60000113a810 ,指向的值的内存地址还是0x600001d3c3b0,最后来到block都执行完的外部再打印一下objc1,地址和一开始的没变化,仍旧是0x7ff7b89e4b18,指向0x600001d3c3b0。
通过上面整个过程可以得出,内存地址0x600001d3c3b0一共被三个obj1所指向,分别是一开始的外部obj1,栈block1里的obj1,还有堆block2里的obj1,所以能得出最上面的一开始的图第二个objc为什么引用计数器是3
然后我们再看看有__block修饰的obj1是如何变化的呢?上面的代码改一下,输出结果如下:
由上图可以看到:
objc1一开始的地址是0x7ff7b4f39b08,指向的值的内存地址是0x600001e8c4b0,然后进入到栈block1里,但是并没有生成一个新的objc1,其地址仍然是0x7ff7b4f39b08,指向的值的内存地址仍然是0x600001d3c3b0,说明了__block修饰的变量指针,并不会被block捕获进行复制,而是直接能调用!所以这时引用计数器并没有增加(指向0x600001d3c3b0的都是同一个objc1),仍然是1。
而第二步栈block1被block2执行copy(模拟ARC下先生成栈block,再copy到堆的操作),堆block2内部又复制出一个新objc1,其地址是0x6000012ed588 ,指向的值的内存地址还是0x600001e8c4b0,理论上假如外部的obj1地址没变,那么0x600001e8c4b0的引用计数应该是2才对,但是为什么block2里输出0x600001e8c4b0的引用计数仍然是1呢?我们来到block都执行完的外部再打印一下objc1看看
发现objc1地址已经变了!和block2里的objc1一样变成了0x6000012ed588,这样我们就能得出结论,objc1经历被栈block1和堆block2分别获取后,指向0x600001e8c4b0的只剩下一个变量指针,不同的是栈block1不会改变objc1的地址,而堆block2则是把objc1的地址改了,原来外部的objc1地址已被系统回收,所以最后只剩下堆block2里的objc1指向了0x600001e8c4b0,引用计数1。解答了我一开始的疑惑🤔
本文正在参加「金石计划」