回调函数是我们在编程中经常使用到的,但是很多新手只知道怎么用,不知其所以然。今天我们就来剖析下回调函数到底是个什么鬼。
先来看一个关于回调函数的形象的比喻
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件
再看下比较正式的定义
callback(回调)就是一段「代码」,我们会通过某种途径,将这段「代码」和一个特定的事件(event)联系起来,当特定事件(event)发生后,这段「代码」被执行。
好了,大家先不管能不能理解上面两种定义,先在脑袋里面有个印象。我们下面来通过实际的例子帮助大家理解回调函数的使用,然后在回头看看上面的定义,定会有拨开乌云见天日的感觉。
为什么要有「回调(callback)」
在这里,斗胆将程序分为两种:
- 「非事件驱动」型程序
- 「事件驱动(event-driven)」型程序
「非事件驱动」型程序。
这类程序,遵循这样一个流程:启动程序 -> 执行程序(代码) -> 退出程序。程序会在执行完所有代码后,立刻退出,中途不会有任何事件(event)发生(除非有bug)。
「事件驱动(event-driven)」型程序
这类程序,遵循这样一个流程:启动程序 -> 等待事件(event) -> 事件被触发 -> 执行callback(回调) -> 继续等待事件(event) -> 人为退出程序。
打个比方,我想用淘宝APP帮手机充值,一打开APP,它并不会马上跳到充值页面,是要等待我的点击事件,当点击了充值的按钮,才会跳到充值页面(执行了callback)。
所以,大家应该很容易联想到,iOS的应用几乎都是「事件驱动(event-driven)」的,应用一经启动,就在等待事件的发生,当发生某个事件(比如点击了某个按钮),应用就会执行某段代码(callback)进行响应。
<font color =red>这里的「事件(event)」,是非常宽泛的,可以是使用者的一次点击、可以是系统的一次通知、可以是服务器返回的一次数据、可以是蓝牙外设连接成功后,发送给手机的一条指令等等。</font>
通过上面我们知道在事件驱动型的程序里面,我们需要当某一个特定条件被触发的时候去调用回调函数来执行某个动作。
下面我们来看看具体的例子。
实例1、数据更新触发回调
如下图所示:
在个人主页我们需要显示用户的昵称、头像、性别、签名,然后点击编辑按钮,跳转到编辑个人信息页面,编辑完个人信息之后,点击返回到主页的时候,我们需要根据用户设定的值来更新相应的信息。
个人主页
编辑用户信息
实现代码如下:
personalHomePage.m文件
**************************
- (void)editPersonalInfo{
EditPersonalInfoController *edit = [[SGEditPersonalInfoController alloc]init];
__weak typeof(self)weakself = self;
edit.updateUserInfoBlock = ^(NSString *userName, NSString *userSex, NSString *userSign ,UIImage *userImage)
{
weakself.userHeaderImageView.image = [userImage circleImage];//用户头像
weakself.nickNameLabel.text = userName;//昵称
weakself.signLabel.text = userSign;//签名
weakself.sexImageView.image = ([userSex isEqualToString:@"女"]) ? [UIImage imageNamed:@"icon_female"] : [UIImage imageNamed:@"icon_male"];//性别
}
}
EditPersonalInfoController.h文件
***********************************
@interface EditPersonalInfoController :UITableViewController
@property(nonatomic, copy)void(^updateUserInfoBlock)(NSString *userName, NSString *userSex, NSString *userSign ,UIImage *userImage) ;
@end
EditPersonalInfoController.m文件
***********************************
- (void)personalInfoHadChanged{
if (self.updateUserInfoBlock) {
self.updateUserInfoBlock(self.userName, self.userSex, self.userSign, self.userImage);
}
}
为了方便叙述,我们做如下规定:
personalHomePage类为A,EditPersonalInfoController为B
1、首先我们在A页面的editPersonalInfo
方法里面跳转到B页面,在这个方法里面我们<font color =red>登记回调函数(block)</font>,这个回调函数的作用就是更新A页面的信息。
2、首先B页面需要<font color =red>申明回调函数</font>,就是@property(nonatomic, copy)void(^updateUserInfoBlock)(NSString *userName, NSString *userSex, NSString *userSign ,UIImage *userImage) ;
,然后B页面在个人信息被更改,这个时候触发了<font color =red>回调关联的事件</font>,调用方法personalInfoHadChanged
方法,在方法里面来<font color =red>调用回调函数</font>,参数就是自己页面更改的信息。
3、一旦回调函数被调用,A页面就<font color =red>响应回调事件</font>,作用就是根据B页面传递过来的参数更改A页面的各个控件的值。
edit.updateUserInfoBlock = ^(NSString *userName, NSString *userSex, NSString *userSign ,UIImage *userImage)
{
weakself.userHeaderImageView.image = [userImage circleImage];//用户头像
weakself.nickNameLabel.text = userName;//昵称
weakself.signLabel.text = userSign;//签名
weakself.sexImageView.image = ([userSex isEqualToString:@"女"]) ? [UIImage imageNamed:@"icon_female"] : [UIImage imageNamed:@"icon_male"];//性别
}
现在再回头把我在这三个步骤里面标记的红色字体部分和文章开头的比喻对照看下,是不是感觉有些明白了。我们再接再厉,多看几个例子,加深理解。
实例2、人为触发回调
除了实例1中提到的当特定的数据(页面数据,服务器返回数据等)被更改的时候需要触发回调函数做一些事情,还有一种情况也需要触发回调函数来做一些处理。
比如用户点击了某个按钮或者页面,我们需要做回调去处理一些逻辑。
具体来看一个实际场景:
如下图所示,是当网络无连接的时候显示的一个view,提示用户可以点击页面进行重新加载。
网络错误View
我们需要给该view添加一个Tap手势,当用户点击的时候我们就触发回调函数,让使用该viwe的控制器去重新加载数据。
代码实现:
NetworkErrorPromptView.h文件
**********************************
@interface NetworkErrorPromptView : UIView
@property(copy,nonatomic)void(^reloadBlock)();//申明回调函数
@end
NetworkErrorPromptView.m文件
**********************************
@implementation NetworkErrorPromptView
-(instancetype)initWithFrame:(CGRect)frame{
if (self == [super initWithFrame:frame]) {
self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil] firstObject];
[self addGestureRecognizer:[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(reload)]];
}
return self;
}
-(void)reload{
if (self.reloadBlock)
{
self.reloadBlock();//调用回调函数
}
}
@end
假设在一个UITableViewController里面使用了该view,使用懒加载初始化该view
-(NetworkErrorPromptView*)errorView{
if (!_errorView) {
_errorView = [[SGNetworkErrorPromptView alloc]init];
_errorView.frame = CGRectMake(0, 0, self.tableView.w, self.tableView.h);
__weak typeof(self) weakself = self;
_errorView.reloadBlock = ^{//登记回调函数,被触发就调用
[weakself.tableView.mj_header beginRefreshing];
};
}
return _errorView;
}
PS:
实例1和实例2非常类似,只是触发回调函数的方式不一样而已,大家理解了实例1,实例2应该不难理解。
所以我就不再一步步的讲解,只是在使用和触发回调函数的地方做了注释
iOS的四种回调模式
其实在iOS里面有四种回调模式,具体如下图所示:
我们上面讲解的是使用block实现回调,因为block的语法看起来有些怪异,所以很多人刚开始使用block实现回调函数就容易出错。
其实block我们可以理解为简版的delegate模式,delegate模式我相信大家理解起来不困难吧,我们经常使用UITableViewController就有很多代理方法。
代理模式就是A页面在特定状态(我们上面说的实例1和2)委托B页面去做某一件事(触发回调函数)。
仔细想一下是不是就是block啊。
如果你不熟悉block的使用,完全可以使用delegate来代替block,用多了就理解回调函数了,然后你想用delegate还是block亦或是notification都可以。(notification少用,影响性能)
总结
我们知道如何使用回调函数,那么我们什么时候才需要用到回调函数呢?
结合上面的实例1和2,我们不难发现,以下场景需要使用回调函数。
如果我们的代码逻辑就是从上到下按顺序执行,中途不需要去做其他的逻辑,那就没必要使用回调函数了。
如果我们需要让只有当某一特定条件满足的时候(数据被更新,用户点击事件),我们才会去执行一段逻辑。这个时候我们就需要使用回调函数来实现了。