本文主要参考:《Object-C 编程 Big Nerd Ranch Guide》一书第24章
本文适读对象:
- 想系统了解iOS中若干种回调机制的朋友;
- 想初步了解Block语法的朋友。
- 没有自己亲自实现过委托、通告、Block进行回调(传递数据)的朋友;
先用一张图总结本文
iOS中的回调(callback)
「回调(callback)」的定义:
“A callback lets you write a piece of code and then associate that code with a particular event. When the event happens, your code is executed.”
——摘自《Object-C Programming:The Big Nerd Ranch Guide 2nd》P613
解读如下:
callback(回调)就是一段「代码」,我们会通过某种途径,将这段「代码」和一个特定的事件(event)联系起来,当特定事件(event)发生后,这段「代码」被执行。
很好,简单粗暴。
为什么要有「回调(callback)」?
「上帝说要有callback,于是就有了callback。」——佚名
在这里,斗胆将程序分为两种:
- 「非事件驱动」型程序
- 「事件驱动(event-driven)」型程序
「非事件驱动」型程序。
这类程序,遵循这样一个流程:启动程序 -> 执行程序(代码) -> 退出程序。程序会在执行完所有代码后,立刻退出,中途不会有任何事件(event)发生(除非有bug)。
比如,我们用Xcode新建一个OS X下的Command Line Tool工具,直接在main.m文件中的main函数写一段从1加到100的代码,然后打印结果出来。如下图:
启动程序->执行代码->退出程序
其中「Program ended with exit code: 0」就表示正确退出了程序。
「事件驱动(event-driven)」型程序
这类程序,遵循这样一个流程:启动程序 -> 等待事件(event) -> 事件被触发 -> 执行callback(回调) -> 继续等待事件(event) -> 人为退出程序。
打个比方,我想用淘宝APP帮手机充值,一打开APP,它并不会马上跳到充值页面,是要等待我的点击事件,当点击了充值的按钮,才会跳到充值页面(执行了callback)。
所以,大家应该很容易联想到,iOS的应用几乎都是「事件驱动(event-driven)」的,应用一经启动,就在等待事件的发生,当发生某个事件(比如点击了某个按钮),应用就会执行某段代码(callback)进行响应。
这里的「事件(event)」,是非常宽泛的,可以是使用者的一次点击、可以是系统的一次通知、可以是服务器返回的一次数据、可以是蓝牙外设连接成功后,发送给手机的一条指令等等。
所以,我们得出结论——上帝说:我们需要callback(回调)。
iOS中的Run loop
我们知道自己需要callback,那在iOS中,具体要怎么实现呢?
首先要有专门的人负责等待事件(event),如果没有这个人,程序就会像「非事件驱动」型程序一样,一个劲地从头跑到尾,就结束了~
这砖找谁搬?
苹果工程师找了一个OC类型的对象,专门干这活儿——等待事件(event)的发生。它就是NSRunLoop实例。看名字就大概能猜到,它会不断循环(loop)。
NSRunLoop实例会持续等待着,当特定事件发生时,触发回调(callback)。
调用以下方法,即可得到一个run loop。
代码语言:javascript复制[[NSRunLoop currentRunLoop] run];
所以,在上述例子中加入一个run loop,这个程序就永远不会退出了(除非人为关闭),有了这个run loop,就可以等待事件(event)的发生了,如下:
添加run loop,等待事件(event)发生
注意,已经没有「Program ended with exit code: 0」——表示成功退出程序这一句了。
当然,新建iOS工程时,已经帮你干这活了,不需要你再手动去实现。
Objective-C中4种实现「回调(callback)」的途径
好了,有了run loop做基础,我们就可以具体去实现iOS中的各种callback(回调)了。
Objective-C中有4种途径可以实现回调:
1、Target-action/目标-动作对
先看代码:
代码语言:javascript复制// 为按钮添加回调——Target-action/目标-动作对
// 第一个参数:发送消息给谁
// 第二个参数:事件发生后,执行什么代码(回调)
// 第三个参数:发生哪类型的点击事件会触发回调
[button addTarget:self
action:@selector(click:)
forControlEvents:UIControlEventTouchUpInside];
以上代码,用人话来讲,大意就是:当按钮被点击后(某事件(event)被触发了),执行本类(self)中的click:方法(回调)。
再看一个NSTimer对象的代码:
代码语言:javascript复制// 一个自定义类对象
Logger *logger = [[Logger alloc] init];
// 为NSTimer对象添加回调——Target-action/目标-动作对
// 第一个参数:发生哪种类型的点击事件会触发回调(这里表示2秒后触发回调)
// 第二个参数:发送消息给一个Logger实例(Logger是自定义的类)
// 第三个参数:事件发生后,执行什么代码(回调)
// 第四个参数:如果有需要传递的数据,可以放在这里
// 第五个参数:这个计时器是否重复执行(也就是说是否重复执行回调)
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:logger
selector:@selector(logSomething:)
userInfo:nil
repeats:YES];
以上代码,用人话来讲就是:创建一个定时器,2秒后(某事件(event)被触发了),执行logger对象所属类的logSomething:方法(回调)。而且是重复地执行。
所以,Target-action/目标-动作对,就是「当事件发生时,向指定的对象发送某个特定的消息」。
以上是书中的描述,但谁是target,谁又是action,搞得含糊不清。所以更倾向于这样理解:
当事件发生时,执行某个类(target)的某个方法(action)。
这里的「某个类」,指的是target参数(例子1是self,例子2是logger)所属类;而「方法」,也就是该类已经实现的某个方法(例子1是click:方法,例子2是logSomething:方法),就是action。
2、Helper objects/辅助对象
「Helper objects/辅助对象」,可以先这样理解:某些功能,找其他类来辅助实现。
常见的就是「delegates/委托」和「/data sources数据源」。下面我们来动手实现一下「delegates/委托」。
先假设有这么一个需求:我们需要用手机通过BLE(低功耗蓝牙)连接8个蓝牙设备,成功连接到8个蓝牙设备后,弹出提示框,提示使用者已经成功连接了多少个蓝牙设备。
先看代码实现:
我们新建一个类,叫MyCnetralManager,专门负责手机和蓝牙模块之前的通讯,这个类的.h文件如下:
代码语言:javascript复制#import <Foundation/Foundation.h>
// 步骤1:声明一份协议(OC中的协议一般写在类中的.h文件)
// 这个协议只有一个方法
@protocol MyCnetralManagerDelegate <NSObject>
// 标记了optional关键字,表示协议中这个方法是可选择性实现(也就是可以不实现)
@optional
/**
* 这个方法通知「被委托对象」,所有设备已经连接上了.
*
* @param devicesCount 传递连接上的设备数量给被委托对象
*/
- (void)allDevicesDidConnected:(NSInteger)devicesCount;
@end
@interface MyCnetralManager : NSObject
// 步骤2:声明delegate属性
@property (weak) id<MyCnetralManagerDelegate> delegate;
@end
.m文件如下:
代码语言:javascript复制#import "MyCnetralManager.h"
// 导入CoreBluetooth蓝牙框架(就是用这个框架进行BLE开发的)
@import CoreBluetooth;
/// 默认需要连接的硬件为8个
const NSInteger defaultDivicesCount = 8;
@interface MyCnetralManager ()<CBCentralManagerDelegate>
/// CBCentralManager对象
@property (strong, nonatomic) CBCentralManager *bleManager;
/// 对已经连接上的设备进行计数
@property (nonatomic) NSInteger connectedDiviceCount;
@end
@implementation MyCnetralManager
// 这里省略蓝牙搜索、连接、发现「服务」、发现「特征」等过程
// 在这里,我们也是应用了官方的「delegates/委托」(CBCentralManagerDelegate),实现发生某些事件后,再执行某些代码(回调)
#pragma mark - CBCentralManagerDelegate
// 这个方法标记了@required,所以一定要实现
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
// (手机)蓝牙状态改变后的回调(比如手机打开蓝牙、关闭蓝牙,都会调用这个方法)
}
// 手机每成功连接一个设备(某事件被触发),这个方法都会被调用(回调)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
// 每连接成功一个设备,计数加1
_connectedDiviceCount ;
// 实现我们自己的「delegates/委托」(MyCnetralManagerDelegate)
// 如果连接上设备数量已经等于事先定义好的数量(8个),就通知委托对象已经连接成功所有设备,并传递连接数量。
if (_connectedDiviceCount == defaultDivicesCount) {
// 步骤3
if ([_delegate respondsToSelector:@selector(allDevicesDidConnected:)]) {
[_delegate allDevicesDidConnected:_connectedDiviceCount];
}
}
}
@end
以上三个步骤,我们已经实现:当连接成功8个蓝牙设备后,向委托对象发送消息allDevicesDidConnected:,并传递一个参数——连接成功设备的数量。
接下来,我们要找到正真干活(显示提示框)的人,找谁呢?找其中一个控制器,如下(某个控制器的.m文件):
我们的目录结构大概如下:
大概会有这两个类
代码语言:javascript复制#import "ViewController.h"
#import "MyCnetralManager.h"
// 遵守协议
@interface ViewController ()<MyCnetralManagerDelegate>
/// 声明一个提示框对象
@property (nonatomic, strong) UIAlertView *alertView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyCnetralManager *manager = [[MyCnetralManager alloc] init];
// 委托谁(找谁干活),就设置delegate是谁
// 初接触委托的人经常会忘记这步
// 如果在XIB文件,也可以通过拖线来完成,就不需要用代码实现
manager.delegate = self;
// 上面这句,可以理解为:MyCnetralManager类委托ViewController类做一些事情。
}
#pragma mark - MyCnetralManagerDelegate
// 实现协议(MyCnetralManagerDelegate)中的方法
// devicesCount是MyCnetralManager类传过来的一个参数
- (void)allDevicesDidConnected:(NSInteger)devicesCount {
// 拿到参数,显示提示框
if (!_alertView) {
_alertView = [[UIAlertView alloc] initWithTitle:@"注意!"
message:[NSString stringWithFormat:@"已经成功连接%@个设备", @(devicesCount)]
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
}
[_alertView show];
}
@end
到此,我们就自己实现了一次简单的委托。
可以翻译成这样的人话:MyCnetralManager委托ViewController做一件事——成功连接所有设备后,显示提示框。
而书上是这样描述的:「当某事件发生时,向遵守相应协议的辅助对象发送消息。」
上述例子可以这样说:「当成功连接8个蓝牙设备后,向遵守MyCnetralManagerDelegate协议的ViewController对象发送allDevicesDidConnected:消息(并传递一个参数)」
为什么不在CBCentralManagerDelegate中的centralManager:didConnectPeripheral:直接弹出提示框提示使用者,而要搞得这么「复杂」?如果有这个疑问,可以移步到我在知乎回答的问题:如何用简单明了的话解释一下什么是 Objective-C 中的委托?或许可以解答你的部分疑问。
至于「data sources/数据源」,常用UITableView的朋友,应该比较熟悉了,本质上和上面讲的委托,一回事儿。(不过我还没有自己实现过~)
3、Notifications/通告
Notification也可以翻译成「通知」,但是为了不和iOS中的「本地通知」、「远程通知」这类「通知」混淆,这里将Notification统一翻译成「通告」,会比较好区分。
实现上面同样的需求,用通告的方式,就会变成这样:
先在MyCnetralManager.m文件中发送通告
代码语言:javascript复制#import "MyCnetralManager.h"
@import CoreBluetooth;
/// 默认需要连接的硬件为8个
const NSInteger defaultDivicesCount = 8;
/// 定义通告的名称
static NSString *kNotificationAllDevicesDidConnected = @"com.YourCompanyName.YourProjectName.AllDevicesDidConnected";
/// 用于创建字典的key
static NSString *totalConnectedDevicesKey = @"totalConnectedDevices";
@interface MyCnetralManager ()<CBCentralManagerDelegate>
/// CBCentralManager对象
@property (strong, nonatomic) CBCentralManager *bleManager;
/// 对已经连接上的设备计数
@property (nonatomic) NSInteger connectedDiviceCount;
@end
@implementation MyCnetralManager
// 在这里,我们也是应用了官方的「delegates/委托」(CBCentralManagerDelegate),实现发生某些事件后,再执行某些代码(回调)
#pragma mark - CBCentralManagerDelegate
// 这个方法标记了@required,所以一定要实现
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
// (手机)蓝牙状态改变后的回调(比如手机打开蓝牙、关闭蓝牙,都会调用这个方法)
}
// 成功连接一个蓝牙设备的回调(官方框架)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
// 每连接成功一个设备,计数加1
_connectedDiviceCount ;
// 如果连接上设备数量已经等于事先定义好的数量(8个),就通知委托对象已经连接成功所有设备,并传递连接数量。
if (_connectedDiviceCount == defaultDivicesCount) {
// 发送通告
// 第一个参数:通告名称
// 第二个参数:谁发送的通告
// 第三个参数:需要传递的额外数据(是一个字典)
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAllDevicesDidConnected
object:nil
userInfo:@{totalConnectedDevicesKey:[NSNumber numberWithInteger:_connectedDiviceCount]}];
}
}
@end
然后在ViewController.m中的viewDidLoad方法内「监测」这个通告:
代码语言:javascript复制- (void)viewDidLoad {
[super viewDidLoad];
// 方案一:传统的selector形式
// 观察通告kNotificationAllDevicesDidConnected,一接收到这个通告,就执行showAlertView:方法(回调)
// 第一个参数:将谁注册为观察者(这里将自己(控制器类自身)注册为观察者)
// 第二个参数:接到通告后,要执行什么方法(代码/回调)
// 第三个参数:接收哪个通告(通告的名称)
// 第四个参数:接收谁发送的通告(nil表示无论谁发送,只要是kNotificationAllDevicesDidConnected,都接收)
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showAlertView:)
name:kNotificationAllDevicesDidConnected
object:nil];
// 方案二:Block形式(Block会在下文展开)
// 用Block语法,使代码更集中、简洁
// 观察通告kNotificationAllDevicesDidConnected,一接收到这个通告,就执行Block中的代码(回调)
/*
[[NSNotificationCenter defaultCenter] addObserverForName:kNotificationAllDevicesDidConnected
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
// 弹出提示框
if (!_alertView) {
_alertView = [[UIAlertView alloc] initWithTitle:@"注意!"
message:[NSString stringWithFormat:@"已经成功连接%@个设备", note.userInfo[totalConnectedDevicesKey]]
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
}
[_alertView show];
}];
*/
}
// 没错,方案一中没有用Block语法,所以这里还要写一个方法
// 方案一中,接收到通告后要执行的方法
- (void)showAlertView:(NSNotification *)note {
// 弹出提示框
if (!_alertView) {
_alertView = [[UIAlertView alloc] initWithTitle:@"注意!"
message:[NSString stringWithFormat:@"已经成功连接%@个设备", note.userInfo[totalConnectedDevicesKey]]
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
}
[_alertView show];
}
所以,苹果提供了一个叫做「通告中心」的对象,可以通过[NSNotificationCenter defaultCenter]
获得,利用这个通告中心,我们可以「发通告」、「监测(接收)通告」,利用这个机制,实现回调。
上面这个例子,可以说成:「当成功连接8个蓝牙设备后,向通告中心发布kNotificationAllDevicesDidConnected通告(一个字符串),并通过userInfo(一个字典)这个参数传递设备的数量;然后通告中心会转发通告出去;这时候在监测该通告的ViewController类收到通告后,就会执行相应的代码(回调)」。
4、Blocks
Block算是Objective-C中比较高阶的内容。这样理解吧,Block其实就是在大括号里面的一大段代码,这段代码,会在某事件(event)发生后被执行。
Block的一些语法
先看看一些Block相关的语法,熟悉一下:
- Block常量:
// Block常量
^{
NSLog(@"我是一个Block常量。^是辨识我身份的标志。记得最后加分号哦,因为我就是一个常量,就像数字「5;」一样");
};
- 带实参、会返回值的Block:
// 有实参,有返回值的Block
^(double dividend, double divisor) {
NSLog(@"我是一个有参数、有返回值的Block");
double quotient = dividend / divisor;
return quotient;
};
- 声明Block变量:
// 声明一个Block变量(无返回值;有参数),
void (^YourBlockName)(id, NSString *, NSUInteger, BOOL *);
// 或
void (^YourBlockName)(id obj, NSString *yourString, NSUInteger deviceCount, BOOL *stop);
- 给Block变量赋值:
// 给你的Block变量赋值
// 等号左边是一个Block变量,等号右边是一个Block常量,将常量赋值给变量
YourBlockName = ^(id array, NSString *theString, NSUInteger count, BOOL *stop){
// Do something what you want.
};
- Block的声明、赋值一起进行:
// 声明、赋值一起
void (^YourBlockName)(id, NSString *, NSUInteger, BOOL *) = ^(id array, NSString *theString, NSUInteger count, BOOL *stop){
// Do something what you want.
};
// 其实就像 int a = 5; 一样(只是Block比较长而已,语法有点怪而已)
- 用C语言的typedef关键字给Block命名为一种新的数据类型(最常用这种形式)。
// 在文件顶部(#import之下)用typedef将Block重新定义为一种新的数据类型
typedef void(^YourBlockName)(id, NSString *, NSUInteger, BOOL *);
// 利用新的数据类型,声明一个Block变量
@property (nonatomic, strong) YourBlockName yourBlock;
// 再对这个变量进行必要的操作(赋值)
以上是关于Block的一些语法,帮助不熟悉的朋友熟悉一下。它其实就是大括号括起来的一段代码,只是语法有点「怪异」而已,而且可以作为方法中的参数进行传递。(在Swift中,与之对应的貌似是「闭包(Closures)」)。
利用Block实现回调
下面,来看一下如何用Block实现回调(实现上面一样的需求):
在MyCnetralManager.h文件
代码语言:javascript复制#import <Foundation/Foundation.h>
@import CoreBluetooth;
// 步骤1:
// 将Block重新定义为一种新的数据类型
// 这个Block无返回值;有一个参数(类型为NSUInteger)
typedef void(^AllDevicesDidConnectedBlock)(NSUInteger divicesCount);
@interface MyCnetralManager : NSObject
// 步骤2:
// 声明一个(Block)变量
@property (nonatomic, strong) AllDevicesDidConnectedBlock callbackForAllDevicesDidConnected;
// 步骤3:
// 声明一个以上述Block作为参数的方法
- (void)callbackForAllDevicesDidConnected:(AllDevicesDidConnectedBlock)allDevicesDidConnectedBlock;
@end
在MyCnetralManager.m文件
代码语言:javascript复制// 成功连接一个蓝牙设备的回调(官方框架)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
// 每连接成功一个设备,计数加1
_connectedDiviceCount ;
if (_connectedDiviceCount == defaultDivicesCount) {
// 步骤4:
// 实现Block回调并进行数据传递
if (self.callbackForAllDevicesDidConnected) {
self.callbackForAllDevicesDidConnected(_connectedDiviceCount);
}
}
}
- (void)callbackForAllDevicesDidConnected:(AllDevicesDidConnectedBlock)allDevicesDidConnectedBlock {
// 步骤5:
// 给我们的Block变量赋值
self.callbackForAllDevicesDidConnected = allDevicesDidConnectedBlock;
}
最后在ViewController.m中的viewDidLoad方法内进行回调:
代码语言:javascript复制- (void)viewDidLoad {
[super viewDidLoad];
_myCentralManager = [[MyCnetralManager alloc] init];
// 利用Block进行回调
// (调用了MyCnetralManager的callbackForAllDevicesDidConnected:方法,传递了一个Block参数)
[_myCentralManager callbackForAllDevicesDidConnected:^(NSUInteger devicesCount) {
NSLog(@"执行了回调:已经成功连接%@个设备", @(devicesCount));
// 弹出提示框
if (!_alertView) {
_alertView = [[UIAlertView alloc] initWithTitle:@"注意!"
message:[NSString stringWithFormat:@"已经成功连接%@个设备", @(devicesCount)]
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
}
[_alertView show];
}];
}
以上是将Block作为一个方法的参数,实现的回调。也可以直接用Block(作为属性)进行回调,如下:
在MyCnetralManager.h文件
代码语言:javascript复制#import <Foundation/Foundation.h>
@import CoreBluetooth;
// 步骤1:
// 将Block重新定义为一种新的数据类型
// 这个Block无返回值;有一个参数(类型为NSUInteger)
typedef void(^AllDevicesDidConnectedBlock)(NSUInteger divicesCount);
@interface MyCnetralManager : NSObject
// 步骤2:
// 声明一个(Block)变量
@property (nonatomic, strong) AllDevicesDidConnectedBlock callbackForAllDevicesDidConnected;
// 其实就是把之前在这里的方法删除
@end
在MyCnetralManager.m文件
代码语言:javascript复制// 成功连接一个蓝牙设备的回调(官方框架)
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
// 每连接成功一个设备,计数加1
_connectedDiviceCount ;
if (_connectedDiviceCount == defaultDivicesCount) {
// 步骤3:
// 利用Block回调并传数据
if (self.callbackForAllDevicesDidConnected) {
self.callbackForAllDevicesDidConnected(_connectedDiviceCount);
}
}
}
最后在ViewController.m中的viewDidLoad方法内进行回调:
代码语言:javascript复制- (void)viewDidLoad {
[super viewDidLoad];
_myCentralManager = [[MyCnetralManager alloc] init];
// 在Block中调用self,可能会导致「引用循环」,所以使用weakSelf
__weak typeof(self) weakSelf = self;
// 直接用Block(MyCnetralManager的属性)回调
_myCentralManager.callbackForAllDevicesDidConnected = ^(NSUInteger devicesCount) {
NSLog(@"执行了回调:已经成功连接%@个设备", @(devicesCount));
// 弹出提示框
if (!weakSelf.alertView) {
_alertView = [[UIAlertView alloc] initWithTitle:@"注意!"
message:[NSString stringWithFormat:@"已经成功连接%@个设备", @(devicesCount)]
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
}
[weakSelf.alertView show];
};
}
两者的区别是:是否多一个方法。不过网上建议使用前者。个人也倾向于使用前者,因为作为方法的参数时,一敲回车,整个Block都会自动补全,而用后者,不会自动补全,要自己一个个敲。
总结
上面,简单实现了Objective-C中的4种回调。
那究竟该使用哪种回调呢?总结书上的建议:
- 当只发生单个事件(event),只需要完成一件事情进行响应,建议用「Target-action/目标-动作对」。比如NSTimer、UIButton等。
- 当会发生若干事件(event),要完成多件事情进行响应,建议使用「Helper objects/辅助对象」,当然了,最常见的是「delegate/委托」(另外还有「data sources/数据源」)。
- 当发生单个事件(event),多个对象要进行响应,建议使用「Notifications/通告」
- Block,当为了写出更简洁的代码、更好的代码结构,建议使用Block(自己总结的)。
以上,就是关于iOS中「回调(callback)」的一些入门级分享。如有谬误,请斧正,谢谢。
尊重劳动成果,转载请注明出处,谢谢。