iOS面试题

IOS基础:通知、KVO与KVC、Block、Delegate(

2020-10-25  本文已影响0人  时光啊混蛋_97boy

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

比较

1、代理和block的选择?

2、Delegate 、Notification和KVO比较各自的优缺点

Delegate优势
Delegate缺点
Notification优势
Notification缺点
KVO优势
KVO缺点

使用

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、注册通知源码解析

源码

/*
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结构体中核心的三个变量以及功能:wildcardnamednameless,在源码中直接用宏定义表示了:WILDCARDNAMELESSNAMED

// 根容器,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是否存在)

  1. 注册通知,如果通知的name存在,则以namekeynamed字典中取出值n(这个n其实被MapNode包装了一层,便于理解这里直接认为没有包装),这个n还是个字典,各种判空新建逻辑不讨论。

  2. 然后以objectkey,从字典n中取出对应的值,这个值就是Observation类型的链表,然后把刚开始创建的Observation对象o存储进去。

    存在name(无论object是否存在)

如果注册通知时传入name,那么会是一个双层的存储结构

  1. 找到NCTable中的named表,这个表存储了还有name的通知
  2. name作为key,找到value,这个value依然是一个map
  3. map的结构是以object作为keyObservation对象为value,这个Observation对象的结构上面已经解释,主要存储了observer & SEL

情况二:只存在object

  1. objectkey,从nameless字典中取出value,此value是个Observation类型的链表
  2. 把创建的Observation类型的对象o存储到链表中

只存在object时存储只有一层,那就是objectObservation对象之间的映射

只存在object

情况三:没有name和object

这种情况直接把Observation对象存放在了Observation *wildcard 链表结构中

3、发送通知源码解析

发送通知的核心逻辑比较简单,基本上就是查找和调用响应方法,从三个存储容器中:namednamelesswildcard去查找对应的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);
}
  1. 通过name & object 查找到所有的Observation对象(保存了observersel),放到数组中
  2. 通过performSelector:逐一调用sel,这是个同步操作
  3. 释放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];
}
  1. 查找时仍然以nameobject为维度的,再加上observer做区分
  2. 因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉

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;
    }
}
  1. 根据coalesceMask参数判断是否合并通知
  2. 接着根据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);
}
  1. runloop触发某个时机,调用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,这两个方法最终都调用了notify()方法
  2. notify()所做的事情就是调用NSNotificationCenterpostNotification:进行发送通知

主线程响应通知

异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?

  1. 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
  2. 在主线程的runloop注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的。

3、注意点

a、页面销毁时不移除通知会崩溃吗

iOS9之前不行,因为notificationcenter对观察者的引用是unsafe_unretained,当观察者释放的时候,观察者的指针值并不为nil,出现野指针导致奔溃。
iOS9之后可以,因为notificationcenter对观察者的引用是weak

b、多次添加同一个通知会是什么结果?多次移除通知呢

会调用多次observeraction
多次移除没有任何影响。

c、下面的方式能接收到通知吗?为什么
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@“TestNotification” object:@1];

// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@“TestNotification” object:nil];

收不到,因为有名字,有observer,在查找对应的notification的时候是在named表中找,找到的不是同一个。通知存储是以nameobject为维度的,即判定是不是同一个通知要从nameobject区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的。

6、UserNotifications的使用

日常生活中会有很多种情形需要通知,比如:新闻提醒、定时吃药、定期体检、到达某个地方提醒用户等等,这些功能在 UserNotifications 中都提供了相应的接口。

iOS推送分为Local Notifications(本地推送) 和 Remote Notifications(远程推送)。
Local Notifications(本地推送):App本地创建通知,加入到系统的Schedule里,如果触发器条件达成时会推送相应的消息内容。

Remote Notifications(远程推送):Provider是指某个APP的Push服务器。APNSApple Push Notification ServiceApple Push服务器)的缩写,是苹果的服务器。

  1. APNS Pusher应用程序把要发送的消息、目的iPhone的标识(deviceToken)打包,发给APNS
  2. APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发到iPhone
  3. iPhone把发来的消息传递给相应的应用程序, 并且按照设定弹出Push通知。

配置

  1. 如果你的App有远端推送的话,那你需要开发者账号的,需要新建一个对应你bundlepush证书。
  2. 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点都推送这个通知,只要repeatsYES就可以了

#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属性 notifyOnEntrynotifyOnExit,是在进入地区、从地区出来或者两者都要的时候进行通知

#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内容了,然后点击sendOK了。

{
  "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(只要保证requestIdentifierok了)。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也没关系,右滑则会出现viewclear选项来触发。

//按钮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来取代movemedia 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

apesmutable-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 Contentinfo.plist中把NSExtensionMainStoryboard替换为NSExtensionPrincipalClass,并且value对应你的类名!

纯代码自定义通知界面

❸ 发送推送
运行工程,将上面的json数据放到APNS Pusher里面点击send,稍等片刻应该能收到消息。

如果你添加了category,需要在Notification contentinfo.plist添加一个键值对UNNotificationExtensionCategoryvalue值和category Actioncategory值保持一致就行。同时在推送json中添加category键值对也要和上面两个地方保持一致。

❹ 本地
远端需要Service Extension 的远端推送,本地的就简单了,只需要在Service ExtensionNotificationService.mdidReceiveNotificationRequest:拿到资源添加到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 Contentinfo.plist中添加一个键值UNNotificationExtensionDefaultContentHidden设置为YES就可以了。


二、KVC

KVC的全称key - value - coding,俗称"键值编码",利用了runtime动态机制,实现的一套通过使用属性名称来间接访问属性的方法。

KVC的原理就是先去调用setKey方法,找不到set方法直接设置属性_keykeyisKey内部会监听到值的改变。

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:的原理
  1. 程序优先调用setKey:属性值方法,代码通过setter方法完成设置。注意,这里的key是指成员变量名,首字母大小写要符合KVC的命名规范
  2. 如果没有找到setKey:方法,KVC机制会检查+(BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认返回的是YES。如果开发者想让这个类禁用KVC,重写了该方法让其返回NO,那么在这一步KVC会执行setValue: forUndefineKey:方法,不过一般不会这么做。所以KVC机制会搜索该类里面有没有名为_key的成员变量,无论该变量是在.h,还是在.m文件里定义,也不论用什么样的访问修饰符,只要存在_key命名的变量,KVC都可以对该成员变量赋值。
  3. 如果该类既没有setKey:方法,也没有_key成员变量,KVC机制会搜索_isKey的成员变量。
  4. 同样道理,如果该类没有setKey:方法,也没有_key_isKey成员变量,KVC还会继续搜索keyisKey的成员变量,再给他们赋值。
  5. 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

简洁的流程描述如下:

+ (BOOL)accessInstanceVariablesDirectly {
      return YES;   // 可以直接访问成员变量
      // return NO;  // 不可以直接访问成员变量,  
}
实践检验
@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的名字是谢佳培

再看一下设置accessInstanceVariablesDirectlyNO的效果:

@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

输出结果显示accessInstanceVariablesDirectlyNO的时候KVC只会查询settergetter这一层,后面寻找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)

设置accessInstanceVariablesDirectlyYES,再修改_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:的原理
  1. 首先按getKeykeyisKey_key的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

  2. 如果上面的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的标准命名方法,包括方法签名。

  3. 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>memberOf<Key>组合的形式调用。

  4. 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>_is<Key><key>is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:方法,默认是抛出异常。

简洁的流程描述如下:

实践检验
@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赋值,需要把数据转为NSNumberNSValue类型传入,那到底哪些类型数据要用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@sum5种,直接看下面例子就明白了。

@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强大作用的又一次体现,KVCObjcruntime组合可以很容易的实现Model和字典的转换。

d、修改一些控件的内部属性

Apple没有提供这访问这些UI控件的API,这样我们就无法正常地访问和修改这些控件的样式。KVC在大多数情况可下可以解决这个问题,最常用的就是个性化UITextField中的placeHolderText了。

e、操作集合

NSArrayNSSet这样的容器类,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)秘籍总结

上一篇下一篇

猜你喜欢

热点阅读