iOS - block原理解读(二)

前言

在阅读该篇文章前,推荐阅读
ios - block原理解读(一)

前情提要

上篇文章理清了block的实现的基本思路,
提到了自动变量中基础类型不能在block内部进行修改。
那么全局变量,全局静态变量,局部静态变量呢?

本文解决问题

  • block中引用静态变量/全局变量/全局静态变量
  • 被block引用的对象,引用计数为何+=2?
  • 循环引用问题、闭环开环的原因

局部静态变量,进行可以修改原值:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        static int a = 10;

        void (^block)(void) = ^{
            a++;
            NSLog(@"%d",a);
        };
        
        block();
        
        return 0;
    }
}

这又是为什么呢?

同样,我们看看编译后的C++代码

这里直接放出和前文不一样的片段。

1.png
2.png

如果你仔细阅读过前文,其他的我就不啰嗦了,
就是从值传递变成了指针传递,
也就是说,block内部将静态变量的地址存储起来,那么用到的时候直接访问其地址就好了。

问:老师‍♂️‍♂️,我有个问题,如果block的作用域 > 这个静态变量会输出什么?

答:静态变量存储在静态区,程序结束后由系统释放,所以不存在block的作用域大于静态变量。

针对全局变量,一张截图你就明白了

全局变量图示.png

全局变量和静态变量小科普:存储同样存储在静态区,由系统管理

所以简单来讲,就是系统针对不同类型的变量的作用域和生命周期,做出了相应的处理。

对象变量

在ARC自动引用计数下,当引用计数为0时,对象会被释放。
当block内部访问该对象时,block对其强引用,

首先,通过两段代码,来看看一个问题:

typedef void (^Block)(void);
Block block;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        TestObject *object = [[TestObject alloc] init];
        
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        block = ^{
            NSLog(@"%@",object);
        };
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        return 0;
    }
}

输出结果:
2019-02-21 20:54:37.526394+0800 BlockTest[70590:3745629] 引用数 1
2019-02-21 20:54:37.527116+0800 BlockTest[70590:3745629] 引用数 3
typedef void (^Block)(void);
Block block;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        TestObject *object = [[TestObject alloc] init];
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        {
            block = ^{
                NSLog(@"%@",object);
            };
        }

        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        return 0;
    }
}

输出结果:
2019-02-21 20:55:50.156887+0800 BlockTest[70627:3749548] 引用数 1
2019-02-21 20:55:50.157928+0800 BlockTest[70627:3749548] 引用数 2

先列出MRC和ARC下block的一点区别
MRC时代的block:
只要block引用外部局部变量,block放在栈里面。
ARC时代的block:
只要block引用外部局部变量,block就放在堆里面。

然后,再看一下c++源码:

image.png

image.png

可以看出:

  1. 对象类型,多出了copy和dispose函数
  2. 原有的栈上的结构体指针被copy到了堆,
    同时,copy函数内部会将栈对象指向堆对象。

如果你对copy函数有疑问,请查看ios - block原理解读(三)

所以,在block初始化作用域内引用计数+2,
在作用域外栈空间的结构体被回收,引用计数-1,
在block消亡后,引用计数-1。

如果你理解了,看一下代码,并说出结果:

typedef void (^Block)(void);
Block block;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        TestObject *object = [[TestObject alloc] init];
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        block = ^{
            NSLog(@"%@",object);
        };
        
        block = nil;
        
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        return 0;
    }
}

答案:1,2

循环引用问题

在ARC大前提下:

  1. block对对象变量强引用
  2. 对象引用计数不为0则不会释放

而所谓循环引用是指,多个对象之间相互引用,产生了闭环。

先上代码:

typedef void (^Block)(void);

@interface ViewController ()

@property (nonatomic,copy) Block block;

@property (nonatomic,copy) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.block = ^{
        NSLog(@"%@",self.name);
    };
}

说明:
viewController现在持有block
通过上文我们已经知道,
block又强引用了当前的的viewController,
那么在ARC环境下,这两个是不会释放的,造成内存泄露。

解决方法

既然造成了闭环,又在想在block中希望使用viewController,
只能将闭环进行断开。

初步方案,看代码:

__weak typeof(self) weakSelf = self;
self.block = ^{
        NSLog(@"%@",weakSelf.name);
};

继续根据c++源码分析

首先,__weak的作用是弱引用,不会增加引用计数,
这个具体原理和__strong,__block在后续继续讲解。

image.png

然后,可以看到结构体内的属性变成同样是__weak类型的,
不会增加引用计数。
所以,下面代码输出结果是:1,1

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        TestObject *object = [[TestObject alloc] init];
        __unsafe_unretained typeof(object) weakObject = object;
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        block = ^{
            NSLog(@"%@",weakObject);
        };
        
        NSLog(@"引用数 %ld",(long)CFGetRetainCount((__bridge CFTypeRef)object));
        
        return 0;
    }
}

所以,上面的闭环状态被我们破坏了,现在仅仅是viewController强引用着block。

安全性

上面的方案,如果block内部执行时间比较长,在执行时,viewController突然被释放了,而block是在堆空间上,并不会被释放,当block内部继续访问viewController,这个时候会出现野指针。

经典解决方案:

__weak typeof(self) weakSelf = self;
self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%@",weakSelf.name);
};

大部分博客只讲到这个解决方案和所谓的短暂的闭环,没有将道理讲明白。

其实,通过上篇文章和上面的解释,我们已经得出了结论。

首先,block引用的外部变量的是__weak修饰的weakSelf对象,
所以block初始化并copy到堆上,不会强引用self。
但是执行block的时候,其实是执行一个静态函数,
在执行的过程中,生成了strongSelf对象,这个时候,产生了闭环。
但是这个strongSelf在栈空间上,在函数执行结束后,strongSelf会被系统回收,此时闭环被打破。

注意:闭环不一定只局限于两个对象,也可能是多个。

最后

以上均为个人研究和理解,如有问题,欢迎评论~
下篇将继续解读,敬请期待!

你可能感兴趣的:(iOS - block原理解读(二))