IOS基础:通知、KVO与KVC、Block、Delegate(
原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 比较
- 使用
- 1、跨层传值
- 2、Block的用法
- 3、Delegate与Protocol的用法
- 一、NSNotification
- 1、关键类结构
- 2、注册通知源码解析
- 3、发送通知源码解析
- 4、删除通知源码解析
- 5、异步通知
- 6、UserNotifications的使用
- 二、KVC
- 1、常见的API
- 2、setValue:forKey: 的原理
- 3、valueForKey: 的原理
- 4、KVC处理异常
- 5、KVC处理数值和结构体类型属性
- 6、KVC键值验证(Key-Value Validation)
- 7、KVC处理集合
- 8、KVC使用场景
- 三、KVO
- 1、KVO 使用
- 2、KVO 实现机制
- 3、KVO的问题
- 4、封装KVO的API
- 5、KVO 防护Crash
- 四、Block
- 1、Block类型
- 2、Block的数据结构 源码解析
- 3、Block捕获变量 源码解析
- 4、Block的循环引用
- 五、Delegate与Protocol
- Demo
- 参考文献
比较
1、代理和block的选择?
- 多个消息传递,应该使用
delegate
,这个时候block
反而不便于维护,而且看起来非常臃肿,很别扭,UITableView
中有很多代理如果都换成block
实现,我们脑海里想一下这个场景是很可怕的。 - 一个委托对象的代理属性只能有一个代理对象,如果想要委托对象调用多个代理对象的回调应该用
block
,因为delegate
只是一个保存某个代理对象的地址,如果设置多个代理相当于重新赋值,只有最后一个设置的代理才会被真正赋值。 - 单例对象最好不要用
delegate
。单例对象由于始终都只是同一个对象,如果使用delegate
,就会造成我们上面说的delegate
属性被重新赋值的问题,最终只能有一个对象可以正常响应代理方法。 - 代理是可选的,而
block
在方法调用的时候只能通过将某个参数传递一个nil
进去,只不过这并不是什么大问题,没有代码洁癖的可以忽略。 - 从设计模式的角度来说,代理更佳面向过程,而
block
更佳面向结果。例如我们使用NSXMLParserDelegate
代理进行XML
解析,NSXMLParserDelegate
中有很多代理方法,NSXMLParser
会不间断调用这些方法将一些转换的参数传递出来,这就是NSXMLParser
解析流程,这些通过代理来展现比较合适。而例如一个网络请求回来,就通过success
、failure
代码块来展示就比较好。 - 从性能上来说,
block
的性能消耗要略大于delegate
,因为block
会涉及到栈区向堆区拷贝等操作,时间和空间上的消耗都大于代理。而代理只是定义了一个方法列表,在运行时向遵守协议的对象发送消息即可。
2、Delegate 、Notification和KVO比较各自的优缺点
Delegate优势
- 如果
delegate
中的一个方法没有实现那么就会出现编译警告/错误 - 一个控制器中可以实现多个不同的协议
- 能够接收返回值
- 一对一的通信
Delegate缺点
- 需要定义很多代码:1) 协议定义 2)
controller
的delegate
属性 3) 实现delegate
方法 - 在释放代理对象时,
delegate
置为nil
,否则内存crash
Notification优势
- 代码量少,实现简单
- 1对多的通信
- 可以携带自定义消息
Notification缺点
- 需要在不用的时候注销通知
- 调试难以追踪
- 通知发送后,不能从观察者得到任何反馈信息
- 代码可读性不强
-
notifacationName
必须相同,否则无法接受消息
KVO优势
- 用
key paths
来观察属性,因此可以观察嵌套对象 - 能够提供一种简单的方法实现两个对象间的同步
- 能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现
KVO缺点
- 观察的属性必须使用
string
来定义,因此编译器不会出现警告 - 对属性重构将导致我们的观察代码不再可用
使用
1、跨层传值
a、属性正向传值
当从第一个页面push
到第二个页面时,第二个页面需要使用到第一个页面的数据,这时就可以使用正向传值。
FirstViewController.m
- (void)proprety
{
SecondViewController *postVC = [[SecondViewController alloc] init];
postVC.content = @"刘盈池";
// 这样传递是有问题的,因为子页面中的textfield是在viewDidLoad中进行初始化和布局的
// 在这时候textfield还没有初始化,为nil,所以赋值是失效的
postVC.contentTextField.text = @"谢佳培";
[self.navigationController pushViewController:postVC animated:YES];
}
SecondViewController.m
- (void)proprety
{
NSLog(@"属性正向传值,content内容为:%@",self.content);
NSLog(@"属性正向传值,contentTextField内容为:%@",self.contentTextField.text);
}
输出结果为:
2020-09-23 17:03:02.553646+0800 Demo[92767:17573694] 属性正向传值,content内容为:刘盈池
2020-09-23 17:03:02.553825+0800 Demo[92767:17573694] 属性正向传值,contentTextField内容为:
b、KVC正向传值
FirstViewController.m
- (void)useKVC
{
SecondViewController *postVC = [[SecondViewController alloc] init];
// 通过Key名给对象的属性赋值,而不需要调用明确的存取方法,这样就可以在运行时动态地访问和修改对象的属性
[postVC setValue:@"刘盈池" forKey:@"content"];
[self.navigationController pushViewController:postVC animated:YES];
}
SecondViewController.m
- (void)useKVC
{
NSLog(@"KVC正向传值,content内容为:%@",self.content);
}
输出结果为:
2020-09-23 17:57:13.984068+0800 Demo[93502:17615940] KVC正向传值,content内容为:刘盈池
c、Delegate逆向传值
在从第二个页面返回第一个页面的时候,第二个页面会释放掉内存,如果需要使用子页面中的数据就用到了逆向传值。
FirstViewController.m
@interface FirstViewController ()<contentDelegate>
- (void)useDelegate
{
SecondViewController *postVC = [[SecondViewController alloc] init];
// 在第一个页面中遵从该代理:第二个页面的代理是第一个页面自身self
postVC.delegate = self;
[self.navigationController pushViewController:postVC animated:YES];
}
// 实现代理中定义的方法,第二个页面调用的时候会回调该方法
- (void)transferString:(NSString *)content
{
// 在方法的实现代码中将参数传递给第一个页面的属性
self.title = content;
NSLog(@"Delegate反向传值,第一个页面接收到的content为:%@",content);
}
SecondViewController.h
// 声明代理
@protocol contentDelegate <NSObject>
/** 代理方法 */
- (void)transferString:(NSString *)content;
@end
@interface SecondViewController : UIViewController
/** 代理属性 */
@property(nonatomic, weak) id<contentDelegate> delegate;
@end
SecondViewController.m
// 返回第一个页面之前调用代理中定义的数据传递方法,方法参数就是要传递的数据
- (void)backDelegate
{
// 如果当前的代理存在,并且实现了代理方法,则调用代理方法进行传递数据
if (self.delegate && [self.delegate respondsToSelector:@selector(transferString:)])
{
[self.delegate transferString:@"刘盈池"];
[self.navigationController popViewControllerAnimated:YES];
}
}
输出结果为:
2020-09-23 17:21:45.923769+0800 Demo[92974:17584680] Delegate反向传值,第一个页面接收到的content为:刘盈池
d、Block逆向传值
FirstViewController.m
- (void)useBlock
{
SecondViewController *postVC = [[SecondViewController alloc] init];
// 通过子页面的block回传拿到数据后进行处理,赋值给当前页面的textfield
postVC.transDataBlock = ^(NSString * _Nonnull content) {
self.title = content;
NSLog(@"Block逆向传值,第一个页面接收到的content为:%@",content);
};
[self.navigationController pushViewController:postVC animated:YES];
}
SecondViewController.h
// block,用于回传数据
typedef void(^TransDataBlock)(NSString *content);
@interface SecondViewController : UIViewController
/** 定义一个block属性,用于回传数据 */
@property(nonatomic, copy) TransDataBlock transDataBlock;
// 或者
@property (nonatomic, copy) void(^ TransDataBlock)(NSString * content);
@end
SecondViewController.m
- (void)backBlock
{
if (self.transDataBlock)
{
self.transDataBlock(@"刘盈池");
}
[self.navigationController popViewControllerAnimated:YES];
}
输出结果为:
2020-09-23 17:29:40.121002+0800 Demo[93133:17594231] Block逆向传值,第一个页面接收到的content为:刘盈池
e、KVO逆向传值
FirstViewController.m
- (void)useKVO
{
self.secondVC = [[SecondViewController alloc] init];
// 在第一个页面注册观察者
[self.secondVC addObserver:self forKeyPath:@"content" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
[self.navigationController pushViewController:self.secondVC animated:YES];
}
// 实现KVO的回调方法,当观察者中的数据有变化时会回调该方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"content"])
{
self.title = self.secondVC.content;
NSLog(@"KVO反向传值,第一个页面接收到的content为:%@",self.secondVC.content);
}
}
- (void)dealloc
{
// 在第一个页面销毁时移除KVO观察者
[self.secondVC removeObserver:self forKeyPath:@"content"];
}
SecondViewController.m
- (void)backKVO
{
// 修改属性的内容
self.content = @"刘盈池";
// 返回第一个界面回传数据
[self.navigationController popViewControllerAnimated:YES];
}
输出结果为:
2020-09-23 17:46:10.744468+0800 Demo[93363:17607725] KVO反向传值,第一个页面接收到的content为:刘盈池
f、Notification反向传值
FirstViewController.m
// 点击跳转到第二个页面
- (void)notificationClick
{
SecondViewController *postVC = [[SecondViewController alloc] init];
[self.navigationController pushViewController:postVC animated:YES];
}
// 1.注册通知
- (void)registerNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(infoAction:) name:@"info" object:nil];
}
// 2.实现收到通知时触发的方法
- (void)infoAction:(NSNotification *)notification
{
NSLog(@"接收到通知,内容为:%@",notification.userInfo);
self.title = notification.userInfo[@"name"];
}
// 3.在注册通知的页面消毁时一定要移除已经注册的通知,否则会造成内存泄漏
- (void)dealloc
{
// 移除所有通知
[[NSNotificationCenter defaultCenter] removeObserver:self];
// 移除某个通知
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"info" object:nil];
// 在第一个页面销毁时移除KVO观察者
[self.secondVC removeObserver:self forKeyPath:@"content"];
}
SecondViewController.m
// 返回到上个界面
- (void)backNotification
{
// 1.创建字典,将数据包装到字典中
NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:@"刘盈池",@"name",@"19",@"age", nil];
// 2.创建通知
NSNotification *notification = [NSNotification notificationWithName:@"info" object:nil userInfo:dict];
// 3.通过通知中心发送通知
[[NSNotificationCenter defaultCenter] postNotification:notification];
// 4.回传数据
[self.navigationController popViewControllerAnimated:YES];
}
输出结果为:
2020-09-23 18:08:28.526933+0800 Demo[93661:17626006] 接收到通知,内容为:{
age = 19;
name = "\U5218\U76c8\U6c60";
}
g、NSUserDefaults传值
如果发送的通知指定了object
对象,那么观察者接收的通知设置的object
对象与其一样,才会接收到通知,但是接收通知如果将这个参数设置为了nil
,则会接收一切通知。
FirstViewController.m
// 需要使用值时通过NSUserDefaults从沙盒目录里面取值进行处理
- (void)useDefaults
{
NSString *content = [[NSUserDefaults standardUserDefaults] valueForKey:@"Girlfriend"];
NSLog(@"NSUserDefaults传值,内容为:%@",content);
}
SecondViewController.m
- (void)userDefaultsClick
{
// 需要传值时将数据通过NSUserDefaults保存到沙盒目录里面,比如用户名之类
// 当用户下次登录或者使用`app`的时候,可以直接从本地读取
[[NSUserDefaults standardUserDefaults] setObject:@"刘盈池" forKey:@"Girlfriend"];
[[NSUserDefaults standardUserDefaults] synchronize];
// 跳转到第一个界面
[self.navigationController popViewControllerAnimated:YES];
}
输出结果为:
2020-09-23 18:19:10.065815+0800 Demo[93846:17636600] NSUserDefaults传值,内容为:刘盈池
2、Block的用法
a、Block的语法
用法
- (void)testBlockGrammer
{
void(^aBlock)(void) = ^{
NSLog(@"齐天大圣孙悟空在此,妖精还不快现原形!");
};
aBlock();
// 拷贝
void(^bBlock)(void) = aBlock;
bBlock();
// 拷贝
void(^cBlock)(void) = [aBlock copy];
cBlock();
// 入参和返回值
int(^sumBlock)(int, int) = ^(int a, int b){
return a + b;
};
int sum = sumBlock(1, 2);
NSLog(@"和为:%d", sum);
}
输出结果
2020-10-16 15:26:05.791836+0800 BlockDemo[94592:3348919] 齐天大圣孙悟空在此,妖精还不快现原形!
2020-10-16 15:26:05.791944+0800 BlockDemo[94592:3348919] 齐天大圣孙悟空在此,妖精还不快现原形!
2020-10-16 15:26:05.792032+0800 BlockDemo[94592:3348919] 齐天大圣孙悟空在此,妖精还不快现原形!
2020-10-16 15:26:05.792104+0800 BlockDemo[94592:3348919] 和为:3
b、捕获变量
用法
static int globalCount = 0;
void testFunc()
{
NSLog(@"天地之间隐有梵音");
}
// 捕获变量
- (void)testBlockCaptureVar
{
// int
int aNumber = 0;
__block int bNumber = 10;
// string
NSString *aStr = @"金鳞岂是池中物";
__block NSString *bStr = @"一遇风云便化龙";
// object
Person *aPerson = [[Person alloc] init];
// block
void(^aBlock)(void) = ^{
NSLog(@"捕获到的全局变量值为:%d", globalCount);
testFunc();// 调用block外的方法
NSLog(@"捕获到的局部字符串值为:%@", aStr);
// 更改捕获到的全局变量值
globalCount = 1;
// 内部定义的int变量和block外的重名了
int aNumber = 1;
// 嵌套block
void(^bBlock)(void) = ^{
NSLog(@"在block内部定义的int变量值为:%d", aNumber);
};
bBlock();
// 更改用block修饰的局部int值
bNumber = 11;
// 更改用block修饰的字符串值
bStr = [bStr stringByAppendingString:@"【聂风 步惊云】"];
// 给对象的属性赋值
aPerson.name = @"谢佳培";
};
aBlock();
NSLog(@"更改后全局变量值为:%d", globalCount);
NSLog(@"更改后用block修饰的局部int值为:%d", bNumber);
NSLog(@"更改后用block修饰的字符串值:%@", bStr);
NSLog(@"给对象的属性赋值后,人物名称为:%@", aPerson.name);
// 说明更改无效
void(^cBlock)(void) = ^{
NSLog(@"内部定义的int变量和block外的重名了,更改外面的值为2后,block捕获到的该值为:%d", aNumber);
};
aNumber = 2;
cBlock();
}
输出结果
2020-10-16 15:45:42.441925+0800 BlockDemo[94869:3361835] 捕获到的全局变量值为:0
2020-10-16 15:45:42.442018+0800 BlockDemo[94869:3361835] 天地之间隐有梵音
2020-10-16 15:45:42.442074+0800 BlockDemo[94869:3361835] 捕获到的局部字符串值为:金鳞岂是池中物
2020-10-16 15:45:42.442144+0800 BlockDemo[94869:3361835] 在block内部定义的int变量值为:1
2020-10-16 15:45:42.442217+0800 BlockDemo[94869:3361835] 更改后全局变量值为:1
2020-10-16 15:45:42.442280+0800 BlockDemo[94869:3361835] 更改后用block修饰的局部int值为:11
2020-10-16 15:45:42.442333+0800 BlockDemo[94869:3361835] 更改后用block修饰的字符串值:一遇风云便化龙【聂风 步惊云】
2020-10-16 15:45:42.442407+0800 BlockDemo[94869:3361835] 给对象的属性赋值后,人物名称为:谢佳培
2020-10-16 15:45:42.442468+0800 BlockDemo[94869:3361835] 内部定义的int变量和block外的重名了,更改外面的值为2后,block捕获到的该值为:0
c、遍历
用法
// 测试block的遍历方法
- (void)testBlockEnumerate
{
// 遍历数组
NSArray<NSString *> *strArray = @[@"谢佳培", @"胡适之", @"丰子恺"];
[strArray enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"索引为:%ld ,内容为:%@", idx, obj);
}];
// 遍历字典
NSDictionary<NSString *, NSString *> *strDict = @{@"student": @"谢佳培", @"teacher": @"郁达夫"};
[strDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"键: %@ ,值: %@", key, obj);
}];
}
输出结果
2020-10-16 15:55:00.657022+0800 BlockDemo[95005:3369183] 索引为:0 ,内容为:谢佳培
2020-10-16 15:55:00.657130+0800 BlockDemo[95005:3369183] 索引为:1 ,内容为:胡适之
2020-10-16 15:55:00.657207+0800 BlockDemo[95005:3369183] 索引为:2 ,内容为:丰子恺
2020-10-16 15:55:00.657281+0800 BlockDemo[95005:3369183] 键: student ,值: 谢佳培
2020-10-16 15:55:00.657348+0800 BlockDemo[95005:3369183] 键: teacher ,值: 郁达夫
d、跨层传值
Person.h文件
// 声明
typedef void(^personBlock)(void);
typedef void(^finishBlock)(NSString *imageURL);
typedef NSString *(^fetchPersonMobileBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, copy) fetchPersonMobileBlock fetchPersonMobileBlock;
/** 运行私有block */
- (void)runPrivatePersonBlock;
/** 运行完成的block */
- (void)runFinishBlock:(finishBlock)finishBlock;// blcok作为入参
/** 打印手机号 */
- (void)printPersonMobile;
@end
Person.m文件
@interface Person()
// 私有block
@property (nonatomic, copy) personBlock privatePersonBlock;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
if (self)
{
_age = 22;
_name = @"谢佳培";
[self initPrivateBlock];
}
return self;
}
// 初始化私有block
- (void)initPrivateBlock
{
__weak Person *weakSelf = self;
self.privatePersonBlock = ^{
// 报错,强引用self
// NSLog(@"person name %@", self.name);
// 报错,强引用self
// NSLog(@"person age %d", _age);
NSLog(@"人名:%@", weakSelf.name);
NSLog(@"年龄:%d", weakSelf.age);
};
}
// 运行私有block
- (void)runPrivatePersonBlock
{
// 调用
self.privatePersonBlock();
}
// 运行完成的block
- (void)runFinishBlock:(finishBlock)finishBlock
{
finishBlock(@"http://.......");
}
// 打印手机号
- (void)printPersonMobile
{
if (self.fetchPersonMobileBlock)
{
NSString *mobile = self.fetchPersonMobileBlock();
NSLog(@"手机号为:%@", mobile);
}
}
@end
测试Person里面的block
- (void)testPersonBlock
{
Person *person = [[Person alloc] init];
[person runPrivatePersonBlock];
[person runFinishBlock:^(NSString *imageURL) {
NSLog(@"获得图片的url为:%@",imageURL);
}];
person.fetchPersonMobileBlock = ^NSString *{
return @"15659281708";
};
[person printPersonMobile];
}
输出结果
2020-10-16 15:14:36.165992+0800 BlockDemo[94449:3340766] 人名:谢佳培
2020-10-16 15:14:36.166096+0800 BlockDemo[94449:3340766] 年龄:22
2020-10-16 15:14:36.166169+0800 BlockDemo[94449:3340766] 获得图片的url为:http://.......
2020-10-16 15:14:36.166226+0800 BlockDemo[94449:3340766] 手机号为:15659281708
3、Delegate与Protocol的用法
a、计算人数工具类
CountTool.h文件
// Delegate
@protocol CountToolDelegate <NSObject>
- (void)willCountAllPerson;// 即将计算人数的委托方法
- (void)didCountedAllPerson;// 完成计算人数的委托方法
@end
// DataSource
@protocol CountToolDataSource <NSObject>
- (NSArray *)personArray;// 返回包含所有人的数组
@end
// 计算人数工具类
@interface CountTool : NSObject
@property (nonatomic, weak) id<CountToolDelegate> delegate;// 委托
@property (nonatomic, weak) id<CountToolDataSource> dataSource;// 数据源
- (void)count;// 计数方法
@end
CountTool.m文件
@implementation CountTool
// 计数方法
- (void)count
{
// 调用即将计数的委托方法
if (self.delegate && [self.delegate conformsToProtocol:@protocol(CountToolDelegate)])
{
[self.delegate willCountAllPerson];
}
// 调用数据源进行计数
NSArray *persons = [self.dataSource personArray];
NSLog(@"人数:%@", @(persons.count));
// 调用完成计数的委托方法
if (self.delegate && [self.delegate respondsToSelector:@selector(didCountedAllPerson)])
{
[self.delegate didCountedAllPerson];
}
}
@end
b、人
工号和职位的协议
#ifndef WorkProtocol_h
#define WorkProtocol_h
@protocol WorkProtocol <NSObject>
@property (nonatomic, strong) NSString *jobNumber;// 工号
@required
- (void)printJobNumber;// 打印工号
@optional
- (void)codingAsProgrammer;// 职位
@end
#endif /* WorkProtocol_h */
Person.h文件
@interface Person : NSObject <WorkProtocol>
@property (atomic, strong, readwrite) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (readonly, strong) NSString *fullName;
@property (nonatomic, strong) NSString *jobNumber;
@property (nonatomic, weak) id<WorkProtocol> delegate;
@end
Person.m文件
@implementation Person
// WorkProtocol
- (void)printJobNumber
{
NSLog(@"打印工号为:%@", self.jobNumber);
}
- (void)codingAsProgrammer
{
NSLog(@"编程者");
}
@end
c、管理者
Administrator.h文件
@interface Administrator : Person
@property (nonatomic, strong, readonly) NSArray *allPersons;// 所有人
- (void)countAllPerson;// 计算所有人的数目
@end
Administrator.m文件
@interface Administrator() <CountToolDelegate, CountToolDataSource>
@property (nonatomic, strong) CountTool *countTool;
@end
@implementation Administrator
// 初始化人物和计算人数工具类
- (instancetype)init
{
self = [super init];
if (self)
{
Person *aPerson = [Person new];
aPerson.firstName = @"xie";
aPerson.lastName = @"jiapei";
Person *bPerson = [[Person alloc] init];
bPerson.firstName = @"fan";
bPerson.lastName = @"yicheng";
_allPersons = @[aPerson, bPerson];
_countTool = [[CountTool alloc] init];
_countTool.delegate = self;
_countTool.dataSource = self;
}
return self;
}
// 计算所有人的数目
- (void)countAllPerson
{
[self.countTool count];// count方法中会调用CountToolDelegate和CountToolDataSource
}
// CountToolDelegate
- (void)willCountAllPerson
{
NSLog(@"调用了即将计算人数的委托方法");
}
- (void)didCountedAllPerson
{
NSLog(@"调用了完成计算人数的委托方法");
}
// CountToolDataSource
- (NSArray *)personArray
{
return self.allPersons;// 返回包含所有人的数组
}
@end
d、调用方式
- (void)viewDidLoad
{
[super viewDidLoad];
Person *aPerson = [[Person alloc] init];
// protocol
aPerson.jobNumber = @"10004847";
[aPerson printJobNumber];
[aPerson codingAsProgrammer];
// delegate, dataSource
Administrator *admin = [[Administrator alloc] init];
[admin countAllPerson];
}
输出结果为:
2020-10-20 17:23:35.042241+0800 DelegateDemo[27449:4938470] 打印工号为:10004847
2020-10-20 17:23:35.042349+0800 DelegateDemo[27449:4938470] 编程者
2020-10-20 17:23:35.042450+0800 DelegateDemo[27449:4938470] 调用了即将计算人数的委托方法
2020-10-20 17:23:35.042539+0800 DelegateDemo[27449:4938470] 人数:2
2020-10-20 17:23:35.042619+0800 DelegateDemo[27449:4938470] 调用了完成计算人数的委托方法
一、NSNotification
1、关键类结构
NSNotification
- (NSString*) name; // 通知的name
- (id) object; // 携带的对象
- (NSDictionary*) userInfo; // 配置信息
NSNotificationCenter
// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 发送通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 删除通知
- (void)removeObserver:(id)observer;
NSNotificationQueue
功能介绍:通知队列,用于异步发送消息,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发时调用NSNotificationCenter
的发送接口进行发送通知,这么看NSNotificationQueue
最终还是调用NSNotificationCenter
进行消息的分发,另外NSNotificationQueue
是依赖runloop
的,所以如果线程的runloop
未开启则无效。
// 把通知添加到队列中,NSPostingStyle是个枚举,下面会介绍
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 删除通知,把满足合并条件的通知从队列中删除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
队列的合并策略和发送时机:把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义。
// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空闲时发送通知
NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的
NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
// 通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默认不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
2、注册通知源码解析
- 判定是不是同一个通知要从
name
和object
区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都以此为基础。 - 存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次
源码
/*
observer:观察者,即通知的接收者
selector:接收到通知时的响应方法
name: 通知name
object:携带对象
*/
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object {
// 前置条件判断
......
// 创建一个observation对象,持有观察者和SEL,下面进行的所有逻辑就是为了存储它
o = obsNew(TABLE, selector, observer);
/*======= case1: 如果name存在 =======*/
if (name) {
//-------- NAMED是个宏,表示名为named字典。以name为key,从named表中获取对应的mapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0) { // 不存在,则创建
m = mapNew(TABLE); // 先取缓存,如果缓存没有则新建一个map
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
...
}
else { // 存在则把值取出来 赋值给m
m = (GSIMapTable)n->value.ptr;
}
//-------- 以object为key,从字典m中取出对应的value,其实value被MapNode的结构包装了一层,这里不追究细节
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0) {// 不存在,则创建
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else {
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
/*======= case2:如果name为空,但object不为空 =======*/
else if (object) {
// 以object为key,从nameless字典中取出对应的value,value是个链表结构
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在则新建链表,并存到map中
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else { // 存在 则把值接到链表的节点上
...
}
}
/*======= case3:name 和 object 都为空 则存储到wildcard链表中 =======*/
else {
o->next = WILDCARD;
WILDCARD = o;
}
}
存储容器
NCTable
结构体中核心的三个变量以及功能:wildcard
、named
、nameless
,在源码中直接用宏定义表示了:WILDCARD
、NAMELESS
、NAMED
// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
Observation *wildcard; /* 链表结构,保存既没有name也没有object的通知 */
GSIMapTable nameless; /* 存储没有name但是有object的通知 */
GSIMapTable named; /* 存储带有name的通知,不管有没有object */
...
} NCTable;
// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct Obs {
id observer; /* 观察者,接收通知的对象 */
SEL selector; /* 响应方法 */
struct Obs *next; /* Next item in linked list. */
...
} Observation;
情况一:存在name(无论object是否存在)
-
注册通知,如果通知的
name
存在,则以name
为key
从named
字典中取出值n
(这个n
其实被MapNode
包装了一层,便于理解这里直接认为没有包装),这个n
还是个字典,各种判空新建逻辑不讨论。 -
然后以
存在name(无论object是否存在)object
为key
,从字典n
中取出对应的值,这个值就是Observation
类型的链表,然后把刚开始创建的Observation
对象o
存储进去。
如果注册通知时传入name
,那么会是一个双层的存储结构
- 找到
NCTable
中的named
表,这个表存储了还有name
的通知 - 以
name
作为key
,找到value
,这个value
依然是一个map
-
map
的结构是以object
作为key
,Observation
对象为value
,这个Observation
对象的结构上面已经解释,主要存储了observer & SEL
情况二:只存在object
- 以
object
为key
,从nameless
字典中取出value
,此value
是个Observation
类型的链表 - 把创建的
Observation
类型的对象o
存储到链表中
只存在object
时存储只有一层,那就是object
和Observation
对象之间的映射
情况三:没有name和object
这种情况直接把Observation
对象存放在了Observation *wildcard
链表结构中
3、发送通知源码解析
发送通知的核心逻辑比较简单,基本上就是查找和调用响应方法,从三个存储容器中:named
、nameless
、wildcard
去查找对应的Observation
对象,然后通过performSelector
:逐一调用响应方法,这就完成了发送流程,核心函数如下:
// 发送通知
- (void) postNotificationName: (NSString*)name
object: (id)object
userInfo: (NSDictionary*)info
{
// 构造一个GSNotification对象, GSNotification继承了NSNotification
GSNotification *notification;
notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
notification->_name = [name copyWithZone: [self zone]];
notification->_object = [object retain];
notification->_info = [info retain];
// 进行发送操作
[self _postAndRelease: notification];
}
//发送通知的核心函数,主要做了三件事:查找通知、发送、释放资源
- (void) _postAndRelease: (NSNotification*)notification {
//step1: 从named、nameless、wildcard表中查找对应的通知
...
//step2:执行发送,即调用performSelector执行响应方法,从这里可以看出是同步的
[o->observer performSelector: o->selector
withObject: notification];
//step3: 释放资源
RELEASE(notification);
}
- 通过
name & object
查找到所有的Observation
对象(保存了observer
和sel
),放到数组中 - 通过
performSelector:
逐一调用sel
,这是个同步操作 - 释放
notification
对象
4、删除通知源码解析
// 删除已经注册的通知
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object {
if (name == nil && object == nil && observer == nil)
return;
...
}
- (void) removeObserver: (id)observer
{
if (observer == nil)
return;
[self removeObserver: observer name: nil object: nil];
}
- 查找时仍然以
name
和object
为维度的,再加上observer
做区分 - 因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉
5、异步通知
上面介绍的NSNotificationCenter
都是同步发送的,接受消息和发送消息是在一个线程里,而这里介绍关于NSNotificationQueue
的异步发送,通过NSNotificationQueue
,将通知添加到队列当中,立即将控制权返回给调用者,在合适的实际发送通知,从而不会阻塞当前的调用。从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop
的时机来触发的。依赖runloop
,所以如果在其他子线程使用NSNotificationQueue
,需要开启runloop
,最终还是通过NSNotificationCenter
进行发送通知,所以这个角度讲它还是同步的,所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程。
入队
/*
* 把要发送的通知添加到队列,等待发送
* NSPostingStyle 和 coalesceMask在上面的类结构中有介绍
* modes这个就和runloop有关了,指的是runloop的mode
*/
- (void) enqueueNotification: (NSNotification*)notification
postingStyle: (NSPostingStyle)postingStyle
coalesceMask: (NSUInteger)coalesceMask
forModes: (NSArray*)modes
{
......
// 判断是否需要合并通知
if (coalesceMask != NSNotificationNoCoalescing) {
[self dequeueNotificationsMatching: notification
coalesceMask: coalesceMask];
}
switch (postingStyle) {
case NSPostNow: {// runloop立即回调通知方法,同步发送
...
// 如果是立马发送,则调用NSNotificationCenter进行发送
[_center postNotification: notification];
break;
}
case NSPostASAP:// runloop在执行timer事件或sources事件的时候回调通知方法,异步发送
// 添加到_asapQueue队列,等待发送
add_to_queue(_asapQueue, notification, modes, _zone);
break;
case NSPostWhenIdle:// runloop空闲的时候回调通知方法,异步发送
// 添加到_idleQueue队列,等待发送
add_to_queue(_idleQueue, notification, modes, _zone);
break;
}
}
- 根据
coalesceMask
参数判断是否合并通知 - 接着根据
postingStyle
参数,判断通知发送的时机。 如果不是立即发送则把通知加入到队列中:_asapQueue
、_idleQueue
,是异步发送。当postingStyle
值是立即发送时,调用的是NSNotificationCenter
进行发送的,所以NSNotificationQueue
还是依赖NSNotificationCenter
进行发送,是同步发送。
发送通知
static void notify(NSNotificationCenter *center,
NSNotificationQueueList *list,
NSString *mode, NSZone *zone)
{
......
// 循环遍历发送通知
for (pos = 0; pos < len; pos++)
{
NSNotification *n = (NSNotification*)ptr[pos];
[center postNotification: n];
RELEASE(n);
}
......
}
// 发送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
notify(item->queue->_center,
item->queue->_asapQueue,
mode,
item->queue->_zone);
}
// 发送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
notify(item->queue->_center,
item->queue->_idleQueue,
mode,
item->queue->_zone);
}
-
runloop
触发某个时机,调用GSPrivateNotifyASAP()
和GSPrivateNotifyIdle()
方法,这两个方法最终都调用了notify()
方法 -
notify()
所做的事情就是调用NSNotificationCenter
的postNotification:
进行发送通知
主线程响应通知
异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?
- 使用
addObserverForName: object: queue: usingBlock
方法注册通知,指定在mainqueue
上响应block
。 - 在主线程的
runloop
注册一个machPort
,它是用来做线程通信的,当在异步线程收到通知,然后给machPort
发送消息,这样肯定是在主线程处理的。
3、注意点
a、页面销毁时不移除通知会崩溃吗
iOS9之前不行,因为notificationcenter
对观察者的引用是unsafe_unretained
,当观察者释放的时候,观察者的指针值并不为nil
,出现野指针导致奔溃。
iOS9之后可以,因为notificationcenter
对观察者的引用是weak
。
b、多次添加同一个通知会是什么结果?多次移除通知呢
会调用多次observer
的action
。
多次移除没有任何影响。
c、下面的方式能接收到通知吗?为什么
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@“TestNotification” object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@“TestNotification” object:nil];
收不到,因为有名字,有observer
,在查找对应的notification
的时候是在named
表中找,找到的不是同一个。通知存储是以name
和object
为维度的,即判定是不是同一个通知要从name
和object
区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的。
6、UserNotifications的使用
日常生活中会有很多种情形需要通知,比如:新闻提醒、定时吃药、定期体检、到达某个地方提醒用户等等,这些功能在 UserNotifications
中都提供了相应的接口。
iOS推送分为Local Notifications
(本地推送) 和 Remote Notifications
(远程推送)。
Local Notifications(本地推送):App本地创建通知,加入到系统的Schedule
里,如果触发器条件达成时会推送相应的消息内容。
Remote Notifications(远程推送):Provider
是指某个APP的Push
服务器。APNS
是Apple Push Notification Service
(Apple Push
服务器)的缩写,是苹果的服务器。
-
APNS Pusher
应用程序把要发送的消息、目的iPhone
的标识(deviceToken
)打包,发给APNS
。 -
APNS
在自身的已注册Push
服务的iPhone
列表中,查找有相应标识的iPhone
,并把消息发到iPhone
。 -
iPhone
把发来的消息传递给相应的应用程序, 并且按照设定弹出Push
通知。
配置
- 如果你的App有远端推送的话,那你需要开发者账号的,需要新建一个对应你
bundle
的push
证书。 -
Capabilities
中打开Push Notifications
开关,打开后会自动在项目里生成entitlements
文件。必须要打开,不然会报错。
Error Domain=NSCocoaErrorDomain Code=3000 "未找到应用程序的“aps-environment”的授权字符串" UserInfo={NSLocalizedDescription=未找到应用程序的“aps-environment”的授权字符串}
a、推送的注册、接收和处理点击事件
#import "AppDelegate.h"
#import "UserNotificationsViewController.h"
#import <UserNotifications/UserNotifications.h>
@interface AppDelegate ()<UNUserNotificationCenterDelegate>
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UserNotificationsViewController *rootVC = [[UserNotificationsViewController alloc] init];
UINavigationController *mainNC = [[UINavigationController alloc] initWithRootViewController:rootVC];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = mainNC;
[self.window makeKeyAndVisible];
[self replyPushNotificationAuthorization:application];
return YES;
}
#pragma mark - 申请通知权限
// 申请通知权限
- (void)replyPushNotificationAuthorization:(UIApplication *)application
{
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
//必须写代理,不然无法监听通知的接收与点击事件
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (!error && granted)
{
//用户点击允许
NSLog(@"注册成功");
}
else
{
//用户点击不允许
NSLog(@"注册失败");
}
}];
//获取权限设置信息,注意UNNotificationSettings是只读对象哦,不能直接修改!
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
NSLog(@"获取权限设置信息:%@",settings);
}];
//注册远端消息通知获取device token
[application registerForRemoteNotifications];
}
#pragma mark - 远端推送需要获取设备的Device Token
//获取DeviceToken成功
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
//解析NSData获取字符串
//错误写法:直接使用下面方法转换为string会得到一个nil(别怪我不告诉你哦)
//NSString *deviceString = [[NSString alloc] initWithData:deviceToken encoding:NSUTF8StringEncoding];
//正确写法
NSString *deviceString = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
deviceString = [deviceString stringByReplacingOccurrencesOfString:@" " withString:@""];
NSLog(@"获取DeviceToken成功:%@",deviceString);
}
//获取DeviceToken失败
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
NSLog(@"获取DeviceToken失败:%@\n",error.description);
}
#pragma mark - 收到通知(苹果把本地通知跟远程通知合二为一)
//App处于前台接收通知时调用,后台模式下是不会走这里的
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{
//收到推送的请求
UNNotificationRequest *request = notification.request;
//收到推送的内容
UNNotificationContent *content = request.content;
//收到用户的基本信息
NSDictionary *userInfo = content.userInfo;
//收到推送消息的角标
NSNumber *badge = content.badge;
//收到推送消息body
NSString *body = content.body;
//推送消息的声音
UNNotificationSound *sound = content.sound;
// 推送消息的副标题
NSString *subtitle = content.subtitle;
// 推送消息的标题
NSString *title = content.title;
if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]])
{
//此处省略一万行需求代码。。。。。。
NSLog(@"收到远程通知:%@",userInfo);
}
else
{
//判断为本地通知
//此处省略一万行需求代码。。。。。。
NSLog(@"iOS10 收到本地通知:{\\\\nbody:%@,\\\\ntitle:%@,\\\\nsubtitle:%@,\\\\nbadge:%@,\\\\nsound:%@,\\\\nuserInfo:%@\\\\n}",body,title,subtitle,badge,sound,userInfo);
}
//不管前台后台状态下。推送消息的横幅都可以展示出来!后台状态不用说,前台时需要在前台代理方法中进行如下设置
//需要执行这个方法,选择是否提醒用户,有Badge、Sound、Alert三种类型可以设置
completionHandler(UNNotificationPresentationOptionBadge|
UNNotificationPresentationOptionSound|
UNNotificationPresentationOptionAlert);
}
//App通知的点击事件。用户点击消息才会触发,如果使用户长按(3DTouch)、弹出Action页面等并不会触发,但是点击Action的时候会触发!
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
//收到推送的请求
UNNotificationRequest *request = response.notification.request;
//收到推送的内容
UNNotificationContent *content = request.content;
//收到用户的基本信息
NSDictionary *userInfo = content.userInfo;
//收到推送消息的角标
NSNumber *badge = content.badge;
//收到推送消息body
NSString *body = content.body;
//推送消息的声音
UNNotificationSound *sound = content.sound;
// 推送消息的副标题
NSString *subtitle = content.subtitle;
// 推送消息的标题
NSString *title = content.title;
if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]])
{
NSLog(@"收到远程通知:%@",userInfo);
//此处省略一万行需求代码。。。。。。
}
else
{
//判断为本地通知
//此处省略一万行需求代码。。。。。。
NSLog(@"iOS10 收到本地通知:{\\\\nbody:%@,\\\\ntitle:%@,\\\\nsubtitle:%@,\\\\nbadge:%@,\\\\nsound:%@,\\\\nuserInfo:%@\\\\n}",body,title,subtitle,badge,sound,userInfo);
}
//系统要求执行这个方法,不然会报completion handler was never called.错误
completionHandler();
}
@end
b、生成本地推送
❶ 创建一个触发器(trigger
)
UNTimeIntervalNotificationTrigger 定时推送:(本地通知)一定时间之后,重复或者不重复推送通知。我们可以设置timeInterval
(时间间隔)和repeats
(是否重复)
//timeInterval:单位为秒(s) repeats:是否循环提醒
//50s后提醒
UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:50 repeats:NO];
UNCalendarNotificationTrigger 定期推送:(本地通知) 一定日期之后,重复或者不重复推送通知 例如,你每天8点推送一个通知,只要dateComponents
为8,如果你想每天8点都推送这个通知,只要repeats
为YES
就可以了
#import <UserNotifications/UserNotifications.h>
//在每周一的14点3分提醒
NSDateComponents *components = [[NSDateComponents alloc] init];
components.weekday = 2;
components.hour = 16;
components.minute = 3;
// components 日期
UNCalendarNotificationTrigger *calendarTrigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES];
UNLocationNotificationTrigger 定点推送:(本地通知)地理位置的一种通知,当用户进入或离开一个地理区域来通知。地区信息使用CLRegion
的子类CLCircularRegion
,可以配置region
属性 notifyOnEntry
和notifyOnExit
,是在进入地区、从地区出来或者两者都要的时候进行通知
#import <CoreLocation/CoreLocation.h>
//创建位置信息
CLLocationCoordinate2D locationCenter = CLLocationCoordinate2DMake(39.788857, 116.5559392);
CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:locationCenter radius:500 identifier:@"经海五路"];
region.notifyOnEntry = YES;
region.notifyOnExit = YES;
//region 位置信息 repeats 是否重复 (CLRegion 可以是地理位置信息)
UNLocationNotificationTrigger *locationTrigger = [UNLocationNotificationTrigger triggerWithRegion:region repeats:YES];
❷ 创建推送的内容(UNMutableNotificationContent
)
//创建通知内容 UNMutableNotificationContent, 注意不是 UNNotificationContent ,此对象为不可变对象
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
//限制在一行,多出部分省略号
content.title = @"《呼啸山庄》 - title";
content.subtitle = [NSString stringWithFormat:@"艾米莉·勃朗特 - subtitle"];
//通知栏出现时,限制在两行,多出部分省略号,但是预览时会全部展示
//body中printf风格的转义字符,比如说要包含%,需要写成%% 才会显示,\同样
content.body = @"一段惊世骇俗的恋情 - body";
content.badge = @5;
content.sound = [UNNotificationSound defaultSound];
content.userInfo = @{@"author":@"Emily Jane Bronte",@"birthday":@"1818年7月30日"};
❸ 创建推送请求(UNNotificationRequest
)
NSString *requestIdentifier = @"xiejiapei";
//创建通知请求 UNNotificationRequest 将触发条件和通知内容添加到请求中
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifier content:content trigger:timeTrigger];
❹ 推送请求添加到推送管理中心(UNUserNotificationCenter
)中
//将通知请求 add 到 UNUserNotificationCenter
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (!error)
{
NSLog(@"推送已添加成功 %@", requestIdentifier);
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"本地通知" message:@"成功添加推送" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
[alert addAction:cancelAction];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
//此处省略一万行需求。。。。
}
}];
c、远端推送
如果你想模拟远端推送,按照我前面介绍的配置环境、证书、push
开关和方法就可以模拟远端的基本远端推送。
❶ 运行工程后会拿到设备的Device Token
,后面会用到。
❷ 需要一个推送服务器APNS pusher
来模拟远端推送服务给APNS
发送信息。
❸ 需要把刚刚获取的device token
填到相应位置,同时要配置好push
证书。
❹ 需要添加aps
内容了,然后点击send
就OK
了。
{
"aps" : {
"alert" : {
"title" : "远程消息,主标题!-title",
"subtitle" : "远程消息,副标题!-Subtitle",
"body" : "why am i so handsome -body"
},
"badge" : "2"
}
}
❺ 稍纵即逝你就收到了远端消息了。
d、Notification Management
❶ Local Notification
需要通过更新request
相同的requestIdentifier
,重新添加到推送center
就可以了,说白了就是重新创建local Notification request
(只要保证requestIdentifier
就ok
了)。Remote Notification
更新需要通过新的字段apps-collapse-id
来作为唯一标示。
❷ 推送消息的查找和删除。
//获取未送达的所有消息列表
- (void)getPendingNotificationRequestsWithCompletionHandler:(void(^)(NSArray<UNNotificationRequest *> *requests))completionHandler;
//删除所有未送达的特定id的消息
- (void)removePendingNotificationRequestsWithIdentifiers:(NSArray<NSString *> *)identifiers;
//删除所有未送达的消息
- (void)removeAllPendingNotificationRequests;
//获取已送达的所有消息列表
- (void)getDeliveredNotificationsWithCompletionHandler:(void(^)(NSArray<UNNotification *> *notifications))completionHandler __TVOS_PROHIBITED;
//删除所有已送达的特定id的消息
- (void)removeDeliveredNotificationsWithIdentifiers:(NSArray<NSString *> *)identifiers __TVOS_PROHIBITED;
//删除所有已送达的消息
- (void)removeAllDeliveredNotifications __TVOS_PROHIBITED;
+ (void)notificationAction
{
NSString *requestIdentifier = @"xiejiapei";
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
//删除设备已收到的所有消息推送
[center removeAllDeliveredNotifications];
//删除设备已收到特定id的所有消息推送
[center removeDeliveredNotificationsWithIdentifiers:@[requestIdentifier]];
//获取设备已收到的消息推送
[center getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * _Nonnull notifications) {
}];
}
e、Notification Actions
❶ 创建Action
可以允许推送添加交互操作 action
,这些 action
可以使得 App 在前台或后台执行一些逻辑代码。如:推出键盘进行快捷回复,该功能以往只在 iMessage
中可行。这叫 category
,是对推送功能的一个拓展,可以通过 3D-Touch
触发,如果你的你的手机不支持3D-Touch
也没关系,右滑则会出现view
和clear
选项来触发。
//按钮action
UNNotificationAction *lookAction = [UNNotificationAction actionWithIdentifier:@"action.join" title:@"接收邀请" options:UNNotificationActionOptionAuthenticationRequired];
UNNotificationAction *joinAction = [UNNotificationAction actionWithIdentifier:@"action.look" title:@"查看邀请" options:UNNotificationActionOptionForeground];
UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive];
//输入框Action
UNTextInputNotificationAction *inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"action.input" title:@"输入" options:UNNotificationActionOptionForeground textInputButtonTitle:@"发送" textInputPlaceholder:@"大声疾呼"];
UNNotificationActionOptions
是一个枚举类型,是用来标识Action
触发的行为方式,分别是:
//需要解锁显示。点击不会进app。
UNNotificationActionOptionAuthenticationRequired = (1 << 0),
//红色文字。点击不会进app。
UNNotificationActionOptionDestructive = (1 << 1),
//黑色文字。点击会进app。
UNNotificationActionOptionForeground = (1 << 2),
❷ 创建category
/**创建category
* identifier:标识符是这个category的唯一标识,用来区分多个category,这个id不管是Local Notification,还是remote Notification,一定要有并且要保持一致
* actions:创建action的操作数组
* intentIdentifiers:意图标识符,可在 <Intents/INIntentIdentifiers.h> 中查看,主要是针对电话、carplay 等开放的API
* options:通知选项,是个枚举类型,也是为了支持carplay
*/
UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"locationCategory" actions:@[lookAction, joinAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
❸ 把category添加到通知中心
// 将 category 添加到通知中心
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center setNotificationCategories:[NSSet setWithObject:notificationCategory]];
//添加Notification Actions
content.categoryIdentifier = @"locationCategory";
[self addNotificationActions];
❹ 把category添加到远端推送
{
"aps" : {
"alert" : {
"title" : "远程消息,主标题!-title",
"subtitle" : "远程消息,副标题!-Subtitle",
"body" : "why am i so handsome -body"
},
"category" : "locationCategory",// 一定要保证键值对一致
"badge" : "2"
}
}
❺ 事件的操作
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
.......
//Notification Actions
NSString* actionIdentifierStr = response.actionIdentifier;
//输入action
if ([response isKindOfClass:[UNTextInputNotificationResponse class]])
{
NSString* userSayStr = [(UNTextInputNotificationResponse *)response userText];
NSLog(@"actionid = %@\n userSayStr = %@",actionIdentifierStr, userSayStr);
//此处省略一万行需求代码。。。。
}
//点击action
if ([actionIdentifierStr isEqualToString:@"action.join"])
{
//此处省略一万行需求代码
NSLog(@"actionid = %@\n",actionIdentifierStr);
}
if ([actionIdentifierStr isEqualToString:@"action.look"])
{
//此处省略一万行需求代码
NSLog(@"actionid = %@\n",actionIdentifierStr);
}
.......
}
f、Media Attachments和自定义推送界面
本地推送和远程推送同时都可支持附带Media Attachments
。不过远程通知需要实现通知服务扩展UNNotificationServiceExtension
,在service extension
里面去下载attachment
,但是需要注意,service extension
会限制下载的时间(30s),并且下载的文件大小也会同样被限制。这里毕竟是一个推送,而不是把所有的内容都推送给用户。所以你应该去推送一些缩小比例之后的版本。比如图片,推送里面附带缩略图,当用户打开app之后,再去下载完整的高清图。视频就附带视频的关键帧或者开头的几秒,当用户打开app之后再去下载完整视频。对图片和视频的大小做了一些限制(图片不能超过 10M,视频不能超过 50M),而且附件资源必须存在本地,如果是远程推送的网络资源需要提前下载到本地。
系统会在通知注册前校验附件,如果附件出问题,通知注册失败。校验成功后,附件会转入attachment data store
。如果附件是在app bundle
,则是会被copy
来取代move
。media attachments
可以利用3d touch
进行预览和操作。
❶ 添加新的Targe--> Notification Service
Notification Service会自动创建一个 UNNotificationServiceExtension
的子类 NotificationService
,通过完善这个子类,来实现你的需求。
#import "NotificationService.h"
#import <UIKit/UIKit.h>
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation NotificationService
#pragma mark - Service
//让你可以在后台处理接收到的推送,传递最终的内容给 contentHandler
//在这里把附件下载保存,然后才能展示渲染
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSString * attchUrl = [request.content.userInfo objectForKey:@"image"];
//下载图片,放到本地
UIImage * imageFromUrl = [self getImageFromURL:attchUrl];
//获取documents目录
NSString * documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString * localPath = [self saveImage:imageFromUrl withFileName:@"MyImage" ofType:@"png" inDirectory:documentsDirectoryPath];
if (localPath && ![localPath isEqualToString:@""])
{
UNNotificationAttachment * attachment = [UNNotificationAttachment attachmentWithIdentifier:@"photo" URL:[NSURL URLWithString:[@"file://" stringByAppendingString:localPath]] options:nil error:nil];
if (attachment)
{
self.bestAttemptContent.attachments = @[attachment];
}
}
// Modify the notification content here...
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
self.contentHandler(self.bestAttemptContent);
}
//在你获得的通知一小段运行代码的时间即将结束的时候,如果仍然没有成功的传入内容,就会走到这个方法
//可以在这里传肯定不会出错的内容,或者会默认传递原始的推送内容
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
}
#pragma mark - Private Methods
- (UIImage *)getImageFromURL:(NSString *)fileURL
{
NSLog(@"执行图片下载函数");
UIImage *result;
//dataWithContentsOfURL方法需要https连接
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
result = [UIImage imageWithData:data];
return result;
}
//将所下载的图片保存到本地
- (NSString *)saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath
{
NSString *urlStr = @"";
if ([[extension lowercaseString] isEqualToString:@"png"])
{
urlStr = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"png"]];
[UIImagePNGRepresentation(image) writeToFile:urlStr options:NSAtomicWrite error:nil];
}
else if ([[extension lowercaseString] isEqualToString:@"jpg"] || [[extension lowercaseString] isEqualToString:@"jpeg"])
{
urlStr = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"jpg"]];
[UIImageJPEGRepresentation(image, 1.0) writeToFile:urlStr options:NSAtomicWrite error:nil];
}
else
{
NSLog(@"extension error");
}
return urlStr;
}
@end
apes
中mutable-content
这个键值为1,意味着此条推送可以被 Service Extension
进行更改,也就是说要使用Service Extension
的话需要加上这个键值为1。
{
"aps":{
"alert" : {
"title" : "呼啸山庄 -title",
"subtitle" : "艾米莉·勃朗特 -Subtitle",
"body" : "说描写吉卜赛弃儿希斯克利夫被山庄老主人收养后,因受辱和恋爱不遂 -body"
},
"sound" : "default",
"badge" : "1",
"mutable-content" : "1",
"category" : "locationCategory"
},
"image" : "https://p1.bpimg.com/524586/475bc82ff016054ds.jpg",
"type" : "scene",
"id" : "1007"
}
❷ 添加新的Targe--> Notification Content
在MainInterface.storyboard
中自定你的UI页面,可以随意发挥,但是这个UI见面只能用于展示,并不能响应点击或者手势其他事件,只能通过category
来实现。
#import "NotificationViewController.h"
#import <UserNotifications/UserNotifications.h>
#import <UserNotificationsUI/UserNotificationsUI.h>
@interface NotificationViewController () <UNNotificationContentExtension>
@property IBOutlet UILabel *label;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation NotificationViewController
// 渲染UI
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
}
// 获取通知信息,更新UI控件中的数据
- (void)didReceiveNotification:(UNNotification *)notification
{
self.label.text = notification.request.content.body;
UNNotificationContent * content = notification.request.content;
UNNotificationAttachment * attachment = content.attachments.firstObject;
if (attachment.URL.startAccessingSecurityScopedResource)
{
self.imageView.image = [UIImage imageWithContentsOfFile:attachment.URL.path];
}
}
@end
有人要有疑问了,可不可以不用storyboard
来自定义界面?当然可以了!只需要在Notifications Content
的info.plist
中把NSExtensionMainStoryboard
替换为NSExtensionPrincipalClass
,并且value
对应你的类名!
❸ 发送推送
运行工程,将上面的json
数据放到APNS Pusher
里面点击send
,稍等片刻应该能收到消息。
如果你添加了category
,需要在Notification content
的info.plist
添加一个键值对UNNotificationExtensionCategory
的value
值和category Action
的category
值保持一致就行。同时在推送json
中添加category
键值对也要和上面两个地方保持一致。
❹ 本地
远端需要Service Extension
的远端推送,本地的就简单了,只需要在Service Extension
的NotificationService.m
的didReceiveNotificationRequest:
拿到资源添加到Notification Content
,在Notification Content
的控制器取到资源自己来做需求处理和展示。
// 资源路径
NSURL *videoURL = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
/**创建附件资源
* identifier 资源标识符
* URL 资源路径
* options 资源可选操作 比如隐藏缩略图之类的
* error 异常处理
*/
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"video.attachment" URL:videoURL options:nil error:nil];
// 将附件资源添加到 UNMutableNotificationContent 中
if (attachment)
{
self.bestAttemptContent.attachments = @[attachment];
}
self.contentHandler(self.bestAttemptContent);
如果你想把default content
隐藏掉,只需要在Notification Content
的info.plist
中添加一个键值UNNotificationExtensionDefaultContentHidden
设置为YES
就可以了。
二、KVC
KVC
的全称key - value - coding
,俗称"键值编码",利用了runtime
动态机制,实现的一套通过使用属性名称来间接访问属性的方法。
KVC
的原理就是先去调用setKey
方法,找不到set
方法直接设置属性_key
、key
、isKey
内部会监听到值的改变。
KVC
是通过NSObject
的扩展NSKeyValueCoding
来实现的,所以对于所有继承了NSObject
的类型,都能使用KVC
。
1、常见的API
key:是直接按照属性的名字设置。
keyPath:一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC
获取该属性,然后再次用KVC
来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC
提供了一个解决方案,那就是键路径keyPath
,相当于根据路径去寻找属性,一层一层往下找。KVC
对于keyPath
是搜索机制第一步就是分离key
,用小数点.
来分割key
,然后再像普通key
一样按照特定的顺序搜索下去。
// 通过Key来设值,如[person setValue:@20 forKey:@"age"];
- (void)setValue:(id)value forKey:(NSString *)key;
// 通过KeyPath来设值,如[person setValue:@20 forKeyPath:@"cat.weight"];
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 直接通过Key来取值,如[person valueForKey:@"age"]
- (id)valueForKey:(NSString *)key;
// 通过KeyPath来取值,如[person valueForKey:@"cat.weight"]
- (id)valueForKeyPath:(NSString *)keyPath;
NSKeyValueCoding
分类中其他的一些方法:
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法里给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
同时苹果对一些容器类比如NSArray
或者NSSet
等,KVC
有着特殊的实现:
//必须实现,对应于NSArray的基本方法
- countOf<Key>
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
- objectIn<Key>AtIndex:
- <key>AtIndexes:
//不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
- get<Key>:range:
//两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
- insertObject:in<Key>AtIndex:
- insert<Key>:atIndexes:
//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
- removeObjectFrom<Key>AtIndex:
- remove<Key>AtIndexes:
//这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之
- replaceObjectIn<Key>AtIndex:withObject:
- replace<Key>AtIndexes:with<Key>:
2、KVC的使用
学校
@interface School : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation School
- (instancetype)init
{
self = [super init];
if (self)
{
self.name = @"Xia Men Da Xue";
self.age = 98;
}
return self;
}
@end
银行账户
struct BankAccount
{
int income;
int balance;
};
typedef struct BankAccount BankAccount;
个人
.h文件
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) NSInteger age;
@property(nonatomic, strong) School *currentSchool;
@property(nonatomic, assign) BankAccount bankAccount;
/** 随机某人 */
+ (Person *)randomPerson;
/** 随机某些人 */
+ (NSArray<Person *> *)randomPersonsWithCount:(NSUInteger)count;
@end
.m文件
@implementation Person
// 默认值
- (instancetype)init
{
self = [super init];
if (self)
{
_name = @"xiejiapei";
_age = 22;
_currentSchool = [[School alloc] init];
BankAccount account = {14000, 3000};
_bankAccount = account;
}
return self;
}
+ (Person *)randomPerson
{
Person *person = [[Person alloc] init];
uint32_t randomAge = arc4random() % 100;
person.age = randomAge;
person.name = [person.name stringByAppendingString:@(randomAge).stringValue];
return person;
}
+ (NSArray<Person *> *)randomPersonsWithCount:(NSUInteger)count
{
NSMutableArray *persons = [NSMutableArray array];
for (int i = 0; i < count; i++)
{
[persons addObject:[self randomPerson]];
}
return [persons copy];// 可变数组转变为不可变
}
@end
使用KVC
单个对象的KVO
- (void)testSinglePerson
{
Person *person = [[Person alloc] init];
// 人物
// get
NSString *originName = [person valueForKey:@"name"];
NSNumber *originAge = [person valueForKey:@"age"];
NSLog(@"获取到的人物的原始名称:%@,原始年龄:%@", originName, originAge);
// set
[person setValue:@"FanYiCheng" forKey:@"name"];
[person setValue:@(18) forKey:@"age"];
NSString *newName = [person valueForKey:@"name"];
NSNumber *newAge = [person valueForKey:@"age"];
NSLog(@"设置后人物的新名称:%@,新年龄:%@", newName, newAge);
// 学校
// keyPath
NSString *originSchoolName = [person valueForKeyPath:@"currentSchool.name"];
NSLog(@"获取到学校的原始名称:%@", originSchoolName);
[person setValue:@"Guo Li Tai Wan Da Xue" forKeyPath:@"currentSchool.name"];
NSString *newSchoolName = [person valueForKeyPath:@"currentSchool.name"];
NSLog(@"设置后学校的新名称 %@", newSchoolName);
// 银行账户
// NSValue
NSValue *originAccountValue = [person valueForKey:@"bankAccount"];
BankAccount originAccount;
[originAccountValue getValue:&originAccount size:sizeof(originAccount)];
NSLog(@"获取到账户的原始收入:%i,支出:%i", originAccount.income, originAccount.balance);
BankAccount newAccount = {18000, 5000};
NSValue *newAccountValue = [NSValue valueWithBytes:&newAccount objCType:@encode(BankAccount)];
[person setValue:newAccountValue forKey:@"bankAccount"];
NSLog(@"设置后账户的新收入:%i,新支出:%i", person.bankAccount.income, person.bankAccount.balance);
}
输出结果为:
2020-09-25 10:19:33.935781+0800 KVO_KVC_Demo[4879:18132700] 获取到的人物的原始名称:xiejiapei,原始年龄:22
2020-09-25 10:19:33.935909+0800 KVO_KVC_Demo[4879:18132700] 设置后人物的新名称:FanYiCheng,新年龄:18
2020-09-25 10:19:33.936010+0800 KVO_KVC_Demo[4879:18132700] 获取到学校的原始名称:Xia Men Da Xue
2020-09-25 10:19:33.936081+0800 KVO_KVC_Demo[4879:18132700] 设置后学校的新名称 Guo Li Tai Wan Da Xue
2020-09-25 10:19:33.936181+0800 KVO_KVC_Demo[4879:18132700] 获取到账户的原始收入:14000,支出:3000
2020-09-25 10:19:33.936274+0800 KVO_KVC_Demo[4879:18132700] 设置后账户的新收入:18000,新支出:5000
集合对象的KVO
- (void)testPersons
{
NSArray<Person *> *persons = [Person randomPersonsWithCount:10];
// 聚合运算符
NSNumber *count = [persons valueForKeyPath:@"@count"];
NSNumber *avgAge = [persons valueForKeyPath:@"@avg.age"];
NSNumber *maxAge = [persons valueForKeyPath:@"@max.age"];
NSNumber *minAge = [persons valueForKeyPath:@"@min.age"];
NSNumber *sumAge = [persons valueForKeyPath:@"@sum.age"];
NSLog(@"总数目:%@,平均年龄:%@,最大年龄:%@,最小年龄:%@, 总年龄:%@",count,avgAge,maxAge,minAge,sumAge);
// 数组运算符
NSArray *distinctAges = [persons valueForKeyPath:@"@distinctUnionOfObjects.age"];
NSArray *unionAges = [persons valueForKeyPath:@"@unionOfObjects.age"];
NSLog(@"年龄差集:%@,年龄并集:%@",distinctAges,unionAges);
// 嵌套运算符
NSArray *personArray1 = [Person randomPersonsWithCount:10];
NSArray *personArray2 = [Person randomPersonsWithCount:10];
NSArray *personArrays = @[personArray1,personArray2];
NSArray *collectedDistinctAges = [personArrays valueForKeyPath:@"@distinctUnionOfArrays.age"];
NSArray *collectedAges = [personArrays valueForKeyPath:@"@unionOfArrays.age"];
NSLog(@"嵌套集合的年龄差集:%@,嵌套集合的年龄并集:%@", collectedDistinctAges,collectedAges);
}
输出结果为:
2020-09-25 10:19:33.936564+0800 KVO_KVC_Demo[4879:18132700] 总数目:10,平均年龄:49.3,最大年龄:84,最小年龄:12, 总年龄:493
// 84重复了
2020-09-25 10:19:33.936720+0800 KVO_KVC_Demo[4879:18132700] 年龄差集:(
13,
31,
84,
32,
63,
46,
55,
12,
73
),年龄并集:(
84,
46,
13,
84,
12,
63,
31,
73,
55,
32
)
// 23重复了
2020-09-25 10:19:33.936951+0800 KVO_KVC_Demo[4879:18132700] 嵌套集合的年龄差集:(
82,
41,
38,
5,
35,
21,
92,
10,
29,
78,
86,
23,
53,
61,
77,
66,
96,
22,
11
),嵌套集合的年龄并集:(
29,
92,
22,
38,
53,
61,
21,
35,
10,
11,
82,
77,
41,
23,
86,
78,
23,
66,
5,
96
)
2、setValue:forKey:
原理探究
setValue:forKey:的原理- 程序优先调用
setKey:
属性值方法,代码通过setter
方法完成设置。注意,这里的key
是指成员变量名,首字母大小写要符合KVC
的命名规范 - 如果没有找到
setKey:
方法,KVC
机制会检查+(BOOL)accessInstanceVariablesDirectly
方法有没有返回YES
,默认返回的是YES
。如果开发者想让这个类禁用KVC
,重写了该方法让其返回NO
,那么在这一步KVC
会执行setValue: forUndefineKey:
方法,不过一般不会这么做。所以KVC
机制会搜索该类里面有没有名为_key
的成员变量,无论该变量是在.h
,还是在.m
文件里定义,也不论用什么样的访问修饰符,只要存在_key
命名的变量,KVC
都可以对该成员变量赋值。 - 如果该类既没有
setKey:
方法,也没有_key
成员变量,KVC
机制会搜索_isKey
的成员变量。 - 同样道理,如果该类没有
setKey:
方法,也没有_key
和_isKey
成员变量,KVC
还会继续搜索key
和isKey
的成员变量,再给他们赋值。 - 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的
setValue:forUndefinedKey:
方法,默认是抛出异常。
简洁的流程描述如下:
- 当我们设置
setValue:forKey:
时 - 首先会查找
setKey:、_setKey:
(按顺序查找) - 如果有直接调用
- 如果没有,先查看
accessInstanceVariablesDirectly
方法
+ (BOOL)accessInstanceVariablesDirectly {
return YES; // 可以直接访问成员变量
// return NO; // 不可以直接访问成员变量,
}
- 如果可以访问会按照
_key、_isKey、key、iskey
的顺序查找成员变量 - 找到直接赋值
- 未找到报错
NSUnkonwKeyException
错误
实践检验
@interface Test: NSObject {
NSString *_name;
}
@end
@implementation Test
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//生成对象
Test *obj = [[Test alloc] init];
//通过KVC赋值name
[obj setValue:@"谢佳培" forKey:@"name"];
//通过KVC取值name打印
NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
}
@end
输出结果显示成功设置和取出obj
对象的name
值:
2020-08-14 15:49:47.729970+0800 Demo[12183:908397] obj的名字是谢佳培
再看一下设置accessInstanceVariablesDirectly
为NO
的效果:
@interface Test: NSObject {
NSString *_name;
}
@end
@implementation Test
+ (BOOL)accessInstanceVariablesDirectly
{
return NO;
}
- (id)valueForUndefinedKey:(NSString *)key
{
NSLog(@"取值出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
NSLog(@"赋值出现异常,该key不存在%@", key);
}
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//生成对象
Test *obj = [[Test alloc] init];
//通过KVC赋值name
[obj setValue:@"谢佳培" forKey:@"name"];
//通过KVC取值name打印
NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
}
@end
输出结果显示accessInstanceVariablesDirectly
为NO
的时候KVC
只会查询setter
和getter
这一层,后面寻找key
的相关变量执行就会停止,直接报错:
2020-08-14 15:57:36.448332+0800 Demo[12237:914275] 赋值出现异常,该key不存在name
2020-08-14 15:57:36.448468+0800 Demo[12237:914275] 取值出现异常,该key不存在name
2020-08-14 15:57:36.448531+0800 Demo[12237:914275] obj的名字是(null)
设置accessInstanceVariablesDirectly
为YES
,再修改_name
为_isName
,看看执行是否成功。
@interface Test: NSObject {
NSString *_isName;
}
@end
+ (BOOL)accessInstanceVariablesDirectly
{
return YES;
}
输出结果显示KVC
会继续按照顺序查找,并成功设值和取值了:
2020-08-14 16:03:13.738074+0800 Demo[12288:918261] obj的名字是谢佳培
3、valueForKey:
原理探究
valueForKey:的原理-
首先按
getKey
、key
、isKey
、_key
的顺序方法查找getter
方法,找到的话会直接调用。如果是BOOL
或者Int
等值类型, 会将其包装成一个NSNumber
对象。 -
如果上面的
getter
没有找到,KVC
则会查找countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法。如果countOf<Key>
方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray
所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子类)。调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray
的方法,就会以countOf<Key>
、objectIn<Key>AtIndex
或<Key>AtIndexes
这几个方法组合的形式调用。还有一个可选的get<Key>:range:
方法。所以你想重新定义KVC
的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC
的标准命名方法,包括方法签名。 -
如果上面的方法没有找到,那么会同时查找
countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet
所的方法的代理集合,和上面一样,给这个代理集合发NSSet
的消息,就会以countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>
组合的形式调用。 -
如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES
(默认行为),那么和先前的设值一样,会按_<key>
、_is<Key>
、<key>
、is<Key>
的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly
返回NO
的话,那么会直接调用valueForUndefinedKey:
方法,默认是抛出异常。
简洁的流程描述如下:
- kvc取值按照
getKey、key、iskey、_key
顺序查找方法 - 存在直接调用
- 没找到同样,先查看
accessInstanceVariablesDirectly
方法 - 如果可以访问会按照
_key、_isKey、key、iskey
的顺序查找成员变量 - 找到直接复制使用
- 未找到报错
NSUnkonwKeyException
错误
实践检验
@interface Test: NSObject
@end
@implementation Test
- (NSUInteger)getAge {
return 22;
}
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//生成对象
Test *obj = [[Test alloc] init];
//通过KVC对age取值
NSLog(@"obj的年龄是%@", [obj valueForKey:@"age"]);
}
@end
输出结果显示[obj valueForKey:@"age"]
,找到了getAge
方法,并且取到了值:
2020-08-14 16:15:59.909067+0800 Demo[12347:926307] obj的年龄是22
把getAge
改成isAge
:
- (NSUInteger)isAge {
return 22;
}
输出结果显示找到了isAge
方法,并且取到了值:
2020-08-14 16:25:56.557110+0800 Demo[12374:931542] obj的年龄是22
4、KVC处理异常
KVC
中最常见的异常就是不小心使用了错误的key
,或者在设值中不小心传递了nil
的值,KVC
中有专门的方法来处理这些异常。
KVC处理nil异常
KVC
不允许要在调用setValue:
属性值forKey:
(或者keyPath
)时对非对象传递一个nil
的值。很简单,因为值类型是不能为nil
的。如果你不小心传了,KVC
会调用setNilValueForKey:
方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
@interface Test: NSObject {
NSUInteger age;
}
@end
@implementation Test
- (void)setNilValueForKey:(NSString *)key
{
NSLog(@"不能将%@设成nil", key);
}
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
[test setValue:nil forKey:@"age"];
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
@end
输出结果为:
2020-08-14 16:36:18.833916+0800 Demo[12421:937814] 不能将age设成nil
2020-08-14 16:36:18.834061+0800 Demo[12421:937814] test的年龄是0
KVC处理UndefinedKey异常
KVC
不允许你要在调用setValue:
属性值forKey:
(或者keyPath
)时对不存在的key
进行操作。不然,会报错forUndefinedKey
发生崩溃,重写forUndefinedKey
方法避免崩溃。
@interface Test: NSObject
@end
@implementation Test
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"取值出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"赋值出现异常,该key不存在%@", key);
}
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
[test setValue:@10 forKey:@"age"];
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
@end
输出结果为:
2020-08-14 16:41:06.407866+0800 Demo[12477:942479] 赋值出现异常,该key不存在age
2020-08-14 16:41:06.408015+0800 Demo[12477:942479] 取值出现异常,该key不存在age
2020-08-14 16:41:06.408089+0800 Demo[12477:942479] test的年龄是(null)
5、KVC处理数值和结构体类型属性
不是每一个方法都返回对象,但是valueForKey:
总是返回一个id
对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber
或者NSValue
对象。这两个类会处理数字、布尔值、指针和结构体的任何类型,然后开发者需要手动转换成原来的类型。
尽管valueForKey:
会自动将值类型封装成对象,但是setValue:forKey:
却不行。你必须手动将值类型转换成NSNumber
或者NSValue
类型,才能传递过去。因为传递进去和取出来的都是id
类型,所以需要开发者自己担保类型的正确性,运行时Objective-C
在发送消息的会检查类型,如果错误会直接抛出异常。
@interface Test: NSObject
@property (nonatomic,assign) NSInteger age;
@end
@implementation Test
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//Test生成对象
Test *test = [[Test alloc] init];
//赋给age的是一个NSNumber对象,KVC会自动的将NSNumber对象转换成NSInteger对象,然后再调用相应的访问器方法设置age的值
[test setValue:[NSNumber numberWithInteger:22] forKey:@"age"];
//会以NSNumber的形式返回age的值
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
@end
输出结果为:
2020-08-14 16:56:19.321904+0800 Demo[12528:950390] test的年龄是22
不能直接将一个数值通过KVC
赋值,需要把数据转为NSNumber
和NSValue
类型传入,那到底哪些类型数据要用NSNumber
封装,哪些类型数据要用NSValue
封装呢?看下面这些方法的参数类型就知道了:
可以使用NSNumber
的数据类型都是一些常见的数值型数据:
+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value;
+ (NSNumber*)numberWithDouble:(double)value;
+ (NSNumber*)numberWithBool:(BOOL)value;
+ (NSNumber*)numberWithInteger:(NSInteger)value;
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)value;
NSValue
主要用于处理结构体型的数据,任何结构体都是可以转化成NSValue
对象的:
+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insets;
6、KVC键值验证(Key-Value Validation)
KVC
提供了验证Key
对应的Value
是否可用的方法:
- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;
该方法默认的实现是调用一个如下格式的方法:
- (BOOL)validate<Key>:error:
这样就给了我们一次纠错的机会。需要指出的是,KVC
是不会自动调用键值验证方法的,就是说我们如果想要键值验证则需要手动验证。但是有些技术,比如CoreData
会自动调用。
@interface Test: NSObject {
NSUInteger _age;
}
@end
@implementation Test
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError
{
NSNumber *age = *ioValue;
if (age.integerValue != 22)
{
return NO;
}
return YES;
}
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//Test生成对象
Test *test = [[Test alloc] init];
NSNumber *age = @22;
NSString *key = @"age";
NSError* error;
BOOL isValid = [test validateValue:&age forKey:key error:&error];
if (isValid)
{
NSLog(@"键值匹配");
//通过KVC设值test的age
[test setValue:age forKey:key];
}
else
{
NSLog(@"键值不匹配");
}
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
@end
当NSNumber *age = @22
时输出结果为:
2020-08-14 17:27:06.495486+0800 Demo[12617:965866] 键值匹配
2020-08-14 17:27:06.495681+0800 Demo[12617:965866] test的年龄是22
当NSNumber *age = @11
时输出结果为:
2020-08-14 17:28:31.928764+0800 Demo[12632:967182] 键值不匹配
2020-08-14 17:28:31.928933+0800 Demo[12632:967182] test的年龄是0
7、KVC处理集合
KVC
同时还提供了很复杂的函数,主要有下面这些:
a、简单集合运算符
有@avg
、@count
、@max
、@min
、@sum
5种,直接看下面例子就明白了。
@interface Book : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) CGFloat price;
@end
@implementation Book
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 10;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 20;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 30;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 40;
NSArray* arrayBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrayBooks valueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f",sum.floatValue);
NSNumber* avg = [arrayBooks valueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f",avg.floatValue);
NSNumber* count = [arrayBooks valueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
NSNumber* min = [arrayBooks valueForKeyPath:@"@min.price"];
NSLog(@"min:%f",min.floatValue);
NSNumber* max = [arrayBooks valueForKeyPath:@"@max.price"];
NSLog(@"max:%f",max.floatValue);
}
@end
输出结果为:
2020-08-14 17:36:25.334395+0800 Demo[12672:972072] sum:100.000000
2020-08-14 17:36:25.334546+0800 Demo[12672:972072] avg:25.000000
2020-08-14 17:36:25.334691+0800 Demo[12672:972072] count:4.000000
2020-08-14 17:36:25.334756+0800 Demo[12672:972072] min:10.000000
2020-08-14 17:36:25.334837+0800 Demo[12672:972072] max:40.000000
b、对象运算符
能以数组的方式返回指定的内容,@distinctUnionOfObjects
、@unionOfObjects
的返回值都是NSArray
,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
@interface Book : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) CGFloat price;
@end
@implementation Book
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 10;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 20;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 20;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 10;
NSArray* arrayBooks = @[book1,book2,book3,book4];
NSLog(@"去重以后的结果");
NSArray* arrayDistinct = [arrayBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
for (NSNumber *price in arrayDistinct) {
NSLog(@"price in arrayDistinct: %f",price.floatValue);
}
NSLog(@"全集");
NSArray* arrayUnion = [arrayBooks valueForKeyPath:@"@unionOfObjects.price"];
for (NSNumber *price in arrayUnion) {
NSLog(@"price in arrayUnion: %f",price.floatValue);
}
}
@end
2020-08-14 17:53:12.183054+0800 Demo[12725:980310] 去重以后的结果
2020-08-14 17:53:12.183228+0800 Demo[12725:980310] price in arrayDistinct: 10.000000
2020-08-14 17:53:12.183296+0800 Demo[12725:980310] price in arrayDistinct: 20.000000
2020-08-14 17:53:12.183486+0800 Demo[12725:980310] 全集
2020-08-14 17:53:12.183563+0800 Demo[12725:980310] price in arrayUnion: 10.000000
2020-08-14 17:53:12.183617+0800 Demo[12725:980310] price in arrayUnion: 20.000000
2020-08-14 17:53:12.183807+0800 Demo[12725:980310] price in arrayUnion: 20.000000
2020-08-14 17:53:12.183985+0800 Demo[12725:980310] price in arrayUnion: 10.000000
c、KVC处理字典
当对NSDictionary
对象使用KVC
时,valueForKey:
的表现行为和objectForKey:
一样。所以使用valueForKeyPath:
用来访问多层嵌套的字典是比较方便的。KVC
里面还有两个关于NSDictionary
的方法:
// 输入一组key,返回这组key对应的属性,再组成一个字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
// 修改Model中对应key的属性
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
实例如下:
@interface Address : NSObject
@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;
@end
@implementation Address
@end
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//模型转字典
Address* address = [Address new];
address.country = @"China";
address.province = @"Fu Jian";
address.city = @"Xia Men";
address.district = @"University";
NSArray* array = @[@"country",@"province",@"city",@"district"];
//把key对应的所有的属性全部取出来
NSDictionary* dict = [address dictionaryWithValuesForKeys:array];
NSLog(@"dictionaryWithValuesForKeys:%@",dict);
//字典转模型
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
//用key Value来修改Model的属性
[address setValuesForKeysWithDictionary:modifyDict];
NSLog(@"country:%@ province:%@ city:%@ district:%@",address.country,address.province,address.city,address.district);
}
@end
输出结果为:
2020-08-14 18:04:39.739856+0800 Demo[12802:988036] dictionaryWithValuesForKeys:{
city = "Xia Men";
country = China;
district = University;
province = "Fu Jian";
}
2020-08-14 18:04:39.739971+0800 Demo[12802:988036] country:USA province:california city:Los angle district:University
8、KVC使用场景
KVC是许多iOS开发黑魔法的基础。
a、动态地取值和设值
利用KVC
动态的取值和设值是最基本的用途了。
b、用KVC来访问和修改私有变量
对于类里的私有属性,Objective-C
是无法直接访问的,但是KVC
是可以的。
c、Model和字典转换
这是KVC
强大作用的又一次体现,KVC
和Objc
的runtime
组合可以很容易的实现Model
和字典的转换。
d、修改一些控件的内部属性
Apple没有提供这访问这些UI控件的API,这样我们就无法正常地访问和修改这些控件的样式。KVC
在大多数情况可下可以解决这个问题,最常用的就是个性化UITextField
中的placeHolderText
了。
e、操作集合
NSArray
和NSSet
这样的容器类,Apple对其valueForKey:
方法作了一些特殊的实现,所以可以用KVC
很方便地操作集合。
f、用KVC实现高阶消息传递
当对容器类使用KVC
时,valueForKey:
将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
- (void)viewDidLoad
{
[super viewDidLoad];
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
}
输出结果为:
2020-08-14 18:19:35.055237+0800 Demo[12847:995929] English
2020-08-14 18:19:35.055328+0800 Demo[12847:995929] Franch
2020-08-14 18:19:35.055391+0800 Demo[12847:995929] Chinese
2020-08-14 18:19:35.055486+0800 Demo[12847:995929] 7
2020-08-14 18:19:35.055546+0800 Demo[12847:995929] 6
2020-08-14 18:19:35.055606+0800 Demo[12847:995929] 7
方法capitalizedString
被传递到NSArray
中的每一项,这样,NSArray
的每一员都会执行capitalizedString
并返回一个包含结果的新的NSArray
。从打印结果可以看出,所有String
都成功以转成了大写。
同样如果要执行多个方法也可以用valueForKeyPath:
方法。它先会对每一个成员调用 capitalizedString
方法,然后再调用length
,因为lenth
方法返回是一个数字,所以返回结果以NSNumber
的形式保存在新数组里。
g、实现KVO
KVO
是基于KVC
实现的。
三、KVO
续文见下篇 IOS基础:通知、KVO与KVC、Block、Delegate(下)
Demo
Demo在我的Github上,欢迎下载。
Notice-Block-Delegate-KVO-KVC
参考文献
轻松过面:一文全解iOS通知机制(经典收藏)
如何自己动手实现 KVO
iOS 开发:『Crash 防护系统』(二)KVO 防护
KVO进阶——KVO实现探究
iOS底层原理总结篇-- 深入理解 KVC\KVO 实现机制
iOS KVC和KVO详解
iOS 10 消息推送(UserNotifications)秘籍总结