block的那些事

iOS基础

Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。一句话来形容Blocks,带有自动变量(局部变量)的匿名函数。

Block的那些事

Block 基础

结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Block_layout {
void *isa; //指向block的类型
int flags; //附加信息
int reserved; //block内部的变量数
void (*invoke)(void *, ...); //函数指针,指向block具体的函数调用地址
struct Block_descriptor *descriptor; //附加描述信息,比如变量数、大小、copy和dispose辅助操作函数指针
/* Imported variables. */
//复制到结构体中的外部局部变量或变量地址
};

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

结构

block中也存在一个isa指针,因此在OC中block也是被当做一个对象来看待的。

Block的类型

NSConcreteStackBlock
  • 只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。
    StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。

  • _NSConcreteStackBlock是不持有对象的。

1
2
3
4
5
6
7
8
9
10
11
//以下是在MRC下执行的
NSObject * obj = [[NSObject alloc]init];
NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);

void (^myBlock)(void) = ^{
NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
};

NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

myBlock();

打印结果:

1
2
3
1.Block外 obj = 1
2.Block外 obj = 1
Block中 obj = 1

由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。
因此,在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。

下面四种情况下会将block从栈自动copy到堆上:

  • 1.手动调用copy
  • 2.Block是函数的返回值
  • 3.Block被强引用,Block被赋值给__strong或者id类型
  • 4.调用系统API入参中含有usingBlcok的方法

注意:copy方法可以将block从栈上copy到堆上,dispose方法可以将堆上的block销毁。

NSConcreteMallocBlock
  • 有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制

  • _NSConcreteMallocBlock是持有对象的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//以下是在MRC下执行的
NSObject * obj = [[NSObject alloc]init];
NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);

void (^myBlock)(void) = [^{
NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
}copy];

NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);

myBlock();

[myBlock release];

NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);

打印结果:

1
2
3
4
1.Block外 obj = 1
2.Block外 obj = 2
Block中 obj = 2
3.Block外 obj = 1
NSConcreteGlobalBlock
  • 没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

  • _NSConcreteGlobalBlock也不持有对象

1
2
3
4
5
6
7
8
9
//以下是在MRC下执行的
void (^myBlock)(void) = ^{

NSObject * obj = [[NSObject alloc]init];
NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
};

myBlock();

打印结果:

1
Block 中 obj = 1
代码演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(void)method { 
//在ARC环境下,Block也是存在__NSStackBlock的时候的,平时见到最多的是_NSConcreteMallocBlock,是因为我们会对Block有赋值操作,所以ARC下,block 类型通过=进行传递时,会导致调用objc_retainBlock->_Block_copy->_Block_copy_internal方法链。并导致 __NSStackBlock__ 类型的 block 转换为 __NSMallocBlock__ 类型。
int a = 3;
void(^block)() = ^{
NSLog(@"调用block%d",a);//这里的变量a,和self.string是一样效果
};
NSLog(@"%@",block);
//打印结果:<__NSMallocBlock__: 0x7fc498746000>
//此时后面的匿名函数赋值给block指针(创建带名字的block),且引用了外部局部变量,block会copy到堆

NSLog(@"%@",^{NSLog(@"调用block%d",a);});
//打印结果:<__NSStackBlock__: 0x7fff54f0c700>
//匿名函数无赋值操作,只存于栈上,会不定释放

static int b = 2;

void(^block)() = ^{
NSLog(@"调用block%d",b);//若不引用任何变量,也是__NSGloBalBlock__
};
NSLog(@"%@",block);
}
//打印结果:<__NSGloBalBlock__: 0x7fc498746000>
//此时引用了全局变量,block放在全局区

Block与循环引用

什么情况下会出现循环引用呢?

单向引用

1
2
3
void (^block1)() = ^{
NSLog(@"%@",self.view);
};

很明显这里是不会出现循环引用的。block虽然强引用了self但是self并没有强引用block

1
2
3
[UIView animateWithDuration:1 animations:^{
NSLog(@"%@",self.view);
}];

这样也不会产生循环引用,因为这是一个类方法,self没办法强引用一个类。

成员变量

1
2
3
self.testBlock = ^(NSString *text) {
NSLog(@"%@",self.view);
};

这里是 self强引用了testblock 同时在block中也强引用了self。因此这回导致循环引用。

类似的还有:

1
2
3
self.testBlock = ^(NSString *text) {
NSLog(@"%@",_arr);
};

即使没有出现self的字眼,这种情况下依然会发生循环引用。

正常情况下,如果出现明显的循环引用,编译器是会给我们提示的

block参数

纯粹的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[self doSomthing1:^{
NSLog(@"str111:%@",self.str);
}];

[self doSomthing2:^(int a, int b) {
NSLog(@"str111:%@",self.str);
}];

- (void)doSomthing1:(void(^)())block{
if(block){
block();
}
}
- (void)doSomthing2:(void (^)(int a, int b))block{
if(block){
block(3,4);
}
}

对于上面的这两种情况,block其实都是作为参数,虽然block中持有了self但是self并没有持有block。因此这里不会产生循环引用的问题。

参数被引用

1
2
3
4
5
6
7
8
9
10
[self doSomthing3:^(NSString *text) {
NSLog(@"str111:%@",self.str);
}];

- (void)doSomthing3:(Block)block{
if(block){
self.testBlock = block;
block(@"111");
}
}

doSomthing3方法中,self对block进行了强引用,这里就会造成循环引用。这种循环引用也称为间接的循环引用,而且这种循环引用编译器是无法提示的。所以,在日常工作中不太容易被发现。

系统自带的block

有时候我们会有一种错觉,系统自带的一些block中使用self不会产生循环引用。

很可惜,这的确是错觉!

1
2
3
[[NSNotificationCenter defaultCenter] addObserverForName:@"testblock" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"%@",self.view);
}];

编译器不会提示有循环引用,但是的的确确这里会产生循环引用。所以还是老老实实的使用weakself,千万不要有这种错觉!

AFN中的循环引用

我们在使用AFN进行网络请求的时候,实际上不需要关注网络回调中可能出现的循环引用,这是因为在AFN的内部做了处理。切断了循环引用链。

那么 我们是否就可以彻底的相信了AFN的处理而不需要自己做处理了呢?

看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)afnBlock
{
AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
mgr.responseSerializer = [AFHTTPResponseSerializer serializer];


//2.1 创建请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://meishubao-static.oss-cn-hangzhou.aliyuncs.com/logs/2018-05/09/121214/bba24fa9ed53970478cdbf640d69620a.zip"]];

NSURLSessionDownloadTask *downloadTask = [mgr downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {//进度

} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
//拼接文件全路径
NSString *fullpath = [caches stringByAppendingPathComponent:response.suggestedFilename];
NSURL *filePathUrl = [NSURL fileURLWithPath:fullpath];
sleep(20);
return filePathUrl;

} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nonnull filePath, NSError * _Nonnull error) {

NSLog(@"%@",self.str);

}];

//3.启动任务
[downloadTask resume];
}

正常情况下,在网络请求的回调中强引用了self,但是self并没有强引用这个网络请求。因此这里不会形成循环引用。

如果self强引用了网络请求(request)且request的回调中也强引用了self是否会造成循环引用呢?

正常情况,这是很明显的循环引用。但是实际上这并不会造成循环引用。因此 AFN肯定在内部做了一些事情。

1
@property (readwrite, nonatomic, strong) NSMutableDictionary *mutableTaskDelegatesKeyedByTaskIdentifier;

我们关注一下AFN的这个属性,其作用是用来保存当前正在进行的所有请求。

开始请求的时候:

1
self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;

将delegate与task进行绑定。

网络请求结束的时候:

1
2
3
4
5
6
7
8
- (void)removeDelegateForTask:(NSURLSessionTask *)task {
NSParameterAssert(task);

[self.lock lock];
[self removeNotificationObserverForTask:task];
[self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];
[self.lock unlock];
}

解除task和delegate的绑定。

尝试一下把下面这句话注释掉:

1
[self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];

你会惊奇的发现,这个网络请求导致了当前进行网络请求的控制器无法被释放!!!

所以,AFN中可以大胆使用self而不用考虑循环引用都是因为这一句。在网络请求成功之后 手动的将self与block与self的引用关系切断。

当然正常情况下,控制器不会持有这个请求。那么是否就表示正常情况下,我们使用AFN的时候完全不用考虑循环应用的情况了呢?

答案是否定的!

来看看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.task = [mgr downloadTaskWithRequest:self.request progress:^(NSProgress * _Nonnull downloadProgress) {//进度

} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
//拼接文件全路径
NSString *fullpath = [caches stringByAppendingPathComponent:response.suggestedFilename];
NSURL *filePathUrl = [NSURL fileURLWithPath:fullpath];
sleep(20);
return filePathUrl;

} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nonnull filePath, NSError * _Nonnull error) {

NSLog(@"%@",self.str);

}];

其实与上面的网络请求唯一的区别是多了一个:

sleep(20);

我们用sleep方法模拟一个特别慢的网络请求。当网络请求开始之后,用户在等待一段时间之后发现网络请求还没有成功,于是退出了当前的页面。

这个时候网络请求没有成功,因此上面我们提到的那个关键的那句话还没有走。这也就导致了这个控制器无法被释放! 因此AFN的这个处理也不是绝对安全的。

为了可以让这个控制器完全释放,我们还是老老实实的使用weakself

Masonry中的block

我们先看一下代码

1
2
3
4
5
6
7
8
9
10
11
- (void)masonryBlock
{
self.lab = [[UILabel alloc] init];
self.lab.textColor = [UIColor redColor];
self.lab.text = @"测试";
[self.view addSubview:self.lab];
[self.lab mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
}];
}

我们都知道循环引用的条件是互相持有,很明显block持有了self那么我们看一下self是否持有block就可以了,我们看一下下面的代码

1
2
3
4
5
6
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

这是view的分类,相当于方法调用中的self.lab。上面的代码我们可以看出self并没有持有block。所以这里绝对不会产生循环引用的问题。

block循环引用 可用之处

在我们使用block的场景中有这样一种场景,为了避免循环引用,我们需要在block中使用weakself,但是我们又希望我们的block被保证可以执行(如果self被释放block中的内容可能无法执行或者部分被执行)。

比如你有一个后台的任务,希望任务执行完后,通知另外一个实例。

我们该怎么做?

这时候 我们可以构造一个循环引用,然后在手动切断这个循环引用

这里我们有两种做法:

第一种: 参考AFN

1
2
3
4
5
6
7
8
9
10
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;

strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}

};

在block内部添加一个对self的强引用,这样就会产生循环应用,这样就可以保证block的内容一定会完整的被执行(如果self被销毁了有可能block被释放了压根不会被执行)。在block执行完成之后 因为strongSelf是一个局部变量在执行完之后会就置为nil,因此循环引用链也会被断开。

第二种: 参考猿题库

1
2
3
4
5
6
//  YTKBaseRequest.m
- (void)clearCompletionBlock {
// nil out to break the retain cycle.
self.successCompletionBlock = nil;
self.failureCompletionBlock = nil;
}

因为block强引用了self,那么如果我们在网络请求结束之后将block置为nil,来破坏到循环引用链那么也可以达到这个效果。

总结

本篇文章总结了在平时工作中常用block相关的基础,后面会在写一篇进阶,主要介绍__block这个关键字。可以在这里看哦!

参考文章