网络回调:Block和Delegate的对比

6,347 阅读10分钟

场景

block和delegate是iOS开发者经常用到的技术,也常常出现在各种面试题里,你经常听到他们之间的对比。

我的态度是每个成熟的技术并没有明显的优劣,不应该用谁好谁劣来评判他们,而应该看谁更适合应用场景,在合适的场合选择合适的技术。

本篇文章将讨论在 网络层调用和回调 这个场景下的技术选择。

本文涉及代码

Block回调

一个常见的Block回调,通常是业务代码调用请求,然后在回调中获得返回的数据,然后执行业务逻辑,如下:

// 业务层代码
- (void)blockDemo {
	[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
	    // 处理业务逻辑代码
	    // ...
		NSLog(@"Result from block:%@", data);
	}];
}

考虑点1:内存

之所以会考虑这个问题,是因为用户使用时常常会刚进入一个页面,就立刻点返回,此时网络请求刚发出去,数据返回有可能还没回来,那么这个网络请求会怎么样呢?

当前页面controller能够被pop出栈,释放掉吗?毕竟网络请求还没结束

我们一一来验证

controller销毁

如何验证内存释放已经释放,很简单,首先我写的网络请求并不是真的网络请求,他只会延时5s返回一个假的数据,用来方便模拟网络请求

- (void)requestWithParms:(NSDictionary *)parms WithResult:(ResultBlock)result {
	
	int delay = NET_DELAY; // 默认是5s,可以传参数改变
	
	if ([parms valueForKey:@"delayTime"] != nil) {
		delay = (int)[parms valueForKey:@"delayTime"];
	}
	
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
		NSString *resultData = @"This is a Mock Data!!";
		NSLog(@"Network Finish:%@",resultData);
		if (result) {
			result(resultData, nil);
		}
	});
}

再在viewcontroller的dealloc方法里打印log,就能知道dealloc是否被调用,如果调用说明可以释放

- (void)dealloc {
	NSLog(@"NextPageViewController has been dealloc!")
}

这样验证起来就是很简单,只需要执行blockDemo方法,然后立刻点返回,退出当前vc,等5秒后如果看结果

结果是vc可以释放的

2018-03-25 19:36:07.345523+0800 NetworkCallback[4580:3600834] NextPageViewController has been dealloc!

Block捕获外界变量销毁

我们再进一步想想,Block有个最大的特点是可以访问当前的作用域,我们随便创建一个数组,重复上面操作,是否能够销毁

- (void)blockDemo {
	NSArray *outsideArray = @[@1, @2, @3];
	
	[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
		// 处理业务逻辑
		// ...
		
		NSLog(@"Result from block:%@", data);
		NSLog(@"outsideArray :%@", outsideArray);
	}];
}

打印结果:

2018-03-25 19:55:40.997535+0800 NetworkCallback[4970:3641450] NextPageViewController has been dealloc!
2018-03-25 19:55:44.831721+0800 NetworkCallback[4970:3641450] outsideArray :(
    1,
    2,
    3
)

神奇不?!

注意,vc先销毁了,但是5s后,这个临时变量竟然还没有销毁。那么这个变量存储在哪里呢?留个悬念

考虑一下,如果你在Block代码里访问了一个超大的文件,这个文件必然是保存内存的,然后此时你遇上了网络慢,接口好久没有返回,那么这个超大的文件就会一直占用内存

继续,如果我在Block中访问self呢?此时的self就是当前的controller,这时候可以销毁吗?

- (void)blockDemo {
	NSArray *outsideArray = @[@1, @2, @3];
	[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
		// 处理业务逻辑
		// ...
		
		NSLog(@"Result from block:%@", data);
		NSLog(@"outsideArray :%@", outsideArray);
		NSLog(@"self :%@", self);
	}];
}

打印结果:

2018-03-25 20:04:21.012135+0800 NetworkCallback[5224:3659292] Network Finish:This is a Mock Data!!
2018-03-25 20:04:21.012476+0800 NetworkCallback[5224:3659292] Result from block:This is a Mock Data!!
2018-03-25 20:04:21.013186+0800 NetworkCallback[5224:3659292] outsideArray :(
    1,
    2,
    3
)
2018-03-25 20:04:21.013675+0800 NetworkCallback[5224:3659292] self :<NextPageViewController: 0x7f993662cf20>
2018-03-25 20:04:21.013858+0800 NetworkCallback[5224:3659292] NextPageViewController has been dealloc!

看到了吗?dealloc是最后打印出来的,也就是说Block不返回,controller就释放不了了!

看起来是在Block内访问谁,谁就无法释放啊!

有人会想这是不是就是循环引用呢?

请大家回忆一下:Block循环引用是self强引用Block,Block里面再强引用self,这里Block确实强引用了self,但是self并没有强引用Block,这个Block是一个参数传给了NetService,跟self并无关系

而且是循环引用的话,那么vc会一直释放不掉,但看上面的log,其实是可以释放的,只是释放的时机被延后了

但是,无论如何,我们试试换成 weakself 会怎么样呢? 打印结果:

2018-03-25 20:07:10.944557+0800 NetworkCallback[5294:3665537] NextPageViewController has been dealloc!
2018-03-25 20:07:15.228961+0800 NetworkCallback[5294:3665537] Network Finish:This is a Mock Data!!
2018-03-25 20:07:15.230836+0800 NetworkCallback[5294:3665537] Result from block:This is a Mock Data!!
2018-03-25 20:07:15.231074+0800 NetworkCallback[5294:3665537] outsideArray :(
    1,
    2,
    3
)
2018-03-25 20:07:15.231190+0800 NetworkCallback[5294:3665537] weakSelf :(null)

没问题,果然换成weakself就解决了

原因是什么?

为什么在Block内访问谁,谁就无法释放呢?为什么用weakself就解决了呢?

Block的本质是个对象

Block看起来像一个函数,其实在objectice-c中,它是个对象,之所以Block可以捕获外部变量,正是因为它是个对象,他有自己的属性,他用属性强引用了外部变量,导致外部变量(就是上面的self和outsideArray)的引用计数不为0,也就不能释放了

weakself做了什么

当Block中访问weakself的时候,强引用并没有指向self,而是指向weakself,所以self可以被释放

内存小结

  1. 使用Block无论是否有循环引用的可能,都要使用weakself,来防止vc被持有,而延迟释放
  2. Block会导致对象的生命周期被延长,特别是当某些大文件被Block访问时,有几率导致内存不足

考虑点2:代码安全

这是基于上面的考虑,我们已经知道要用weakself来保证controller被及时释放,也可以在上面log中看到weakself变成了nil,此时有可能导致crash,因为我们正在操作一个nil对象 想象一个业务场景:分页请求,你拉取了前面几页,比如page=3,然后去拉下一页数据,此时网络请求尚未返回,用户就退出当前页面,此时

  1. 如果页面能够被释放,那么Block中的业务逻辑代码被执行吗?
  2. 如果可以执行会有什么危险?

其实第一个问题上面的log已经回答了,log之所以被打印出来,其实就是Block中的代码被执行了嘛。

也就是说即使controller已经销毁,Block中的代码还是会被执行

第二个问题,执行了会有什么危险

  1. 通常这里会做json转model,会做某些数据转换,如果返回数据很大,比如是个三千多个元素的数组,那么势必浪费CPU去执行,注意,此时controller已经销毁了,执行代码是无意义的,这里的CPU是确确实实的浪费掉了。
  2. 想像一下,此时weakself是nil,如果是分页请求的数据,你通常是把新的数据加到某个数组里,然后你就crash了,因为你把nil加到数组去了
- (void)blockDemo {
	__weak typeof(self) weakSelf = self;
	NSArray *outsideArray = @[@1, @2, @3];
	[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
		// 处理业务逻辑
		// ...
		[data addObject:weakSelf.pageArray]; // weakSelf是nil
	}];
}

因此,你必须小心翼翼,写上的保护代码

- (void)blockDemo {
	__weak typeof(self) weakSelf = self;
	NSArray *outsideArray = @[@1, @2, @3];
	[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
		
		// 保护代码
		if (weakSelf == nil) {
			return;
		}
		
		// 处理业务逻辑
		// ...
	}];
}

代码安全总结

  1. Block会有执行无意义代码的可能,浪费CPU
  2. Block会有操作nil对象导致crash的可能,因此要写保护代码

Delegate回调

经过上面的验证,看起来好像Block有挺多麻烦了,那么delegate怎么样呢?我们也来试一试

首先是模拟网络请求,然后通过delegate回调

- (void)requestWithParms:(NSDictionary *)parms {
	int delay = NET_DELAY;
	
	if ([parms valueForKey:@"delayTime"] != nil) {
		delay = (int)[parms valueForKey:@"delayTime"];
	}
	
	dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
		
		// 判断成功
		// 判断失败
		
		NSString *resultData = @"This is a Mock Data!!";
		NSLog(@"Network Finish:%@",resultData);
		
		// ***这里加了判断***
		if (self.delegate && [self.delegate respondsToSelector:@selector(networkFinishWithSuccess:AndError:)]) {
			[self.delegate networkFinishWithSuccess:resultData AndError:nil];
		}
	});
}

注意我用***标注的注释,这里有个delegate的判断,这里保证了Block考虑点2不会出现,因为当delegate为nil的时候,绝对不会执行delegate的方法.

演示一下,代码如下

#pragma mark - Delegate request
- (void)delegateDemo {
	self.service.delegate = self;
	[self.service requestWithParms:nil];
}

#pragma mark - NetWorkDelegate
- (void)networkFinishWithSuccess:(id)data AndError:(NSError *)error {
	NSLog(@"Result from Delegate:%@", data);
}

验证block存在的问题

同样的,一进入Nextpage就立刻点返回,等5s看看代码会不会执行

2018-03-25 21:08:23.422103+0800 NetworkCallback[6399:3784145] NextPageViewController has been dealloc!

只有一条log,说明内存释放没有问题,而且在回调前对delegate的判断,使得我们非常方便的得知业务层是否还存在了,而如果用Block来实现就很麻烦,在网络回调前是无法得知的,一定要在Block里面加判断代码

总结:

  1. 在业务层delegate比Block更加优雅,可以在网络层回调前就中断逻辑,把错误发生的可能提前中断,而不必进入业务层才做判断,这是一个很好的隔断
  2. 没有延长某个对象生命周期,代码更加清晰,易于管理

delegate自己的问题

那么难道delegate就没有缺点了吗?

多个业务层请求

之前的demo中只有一个业务层,工程中绝对不会只有一个,而NetService的delegate只能指向一个对象,岂不是只有一个请求能够拿到回调,这岂不是滑天下之大稽?

当然不能这样,如果使用delegate,就必须对每个请求封装成一个对象,而不能统一的用一个NetService

@interface RequestAPI : NSObject

@property (nonatomic, weak, nullable) id<NetWorkDelegate> delegate;

/**
 模拟Delegate请求方法
 
 @param parms 请求参数
 */
- (void)requestWithParms:(NSDictionary *)parms;

可是每个对象都去实现一篇请求逻辑岂不是很傻?!

所以底层还是调用NetService


#import "RequestAPI.h"

@implementation RequestAPI

- (void)requestWithParms:(NSDictionary *)parms {
	__weak typeof(self) weakSelf = self;
	[[NetService alloc] requestWithParms:parms WithResult:^(id  _Nonnull data, NSError * _Nonnull error) {
		if (weakSelf && [weakSelf respondsToSelector:@selector(networkFinishWithSuccess:AndError:)]) {
			[self.delegate networkFinishWithSuccess:resultData AndError:nil];
		}
	}];
}

总结

所以其实,使用delegate和Block的结合使用,由此我们可以看出

  1. Block适合做集约型调用,每个业务逻辑不一样,但是我们可以通过把代码封装在Block中,然后发给统一的方法来处理,实现了统一方法处理不同的逻辑
  2. delegate适合离散型调用,每次返回是同样的逻辑
  3. 网络层调用要delegate和Block结合使用,在业务层回调适合delegate,在底层网络处理适合Block

参考: AFNetworking Casa Taloyum