iOS 老生常谈之Block(里面包含其他知识点延伸)

232 阅读8分钟

本文正在参加「金石计划」

前阵子看到一条关于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修饰只是能读取,私以为引用计数的机制都是一样的,找了很多资料,基本都没找到相关解释,终于在一篇有点擦边的文章看到这么一段话:

2.png

看到这句话时,我得到了一些启发,得出了最后为什么输出的1,如果我的想法不正确,欢迎大家指正我^ _ ^

首先我完整给一下这个题目的解答过程

第一个输出1:

NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));

obj初始化,引用计数 +1,所以输出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修饰的整个实现过程:

3.png

由上图可以看到:

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是如何变化的呢?上面的代码改一下,输出结果如下:

4.png 由上图可以看到:

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。解答了我一开始的疑惑🤔

本文正在参加「金石计划」