iOS 面试第七节 消息传递的方式

w@TOC

1. KVC实现原理

  • KVC,键-值编码,使用字符串直接访问对象的属性。
  • 底层实现,当一个对象调用setValue方法时,方法内部会做以下操作:

1.检查是否存在相应key的set方法,如果存在,就调用set方法
2.如果set方法不存在,就会查找与key相同名称并且带下划线的成员属性,如果有,则直接给成员属性赋值
3.如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
4.在顺序执行的时候通过accessInstanceVariablesDirectly 判定是否继续查找下一个是否存在的元素。默认YES就是将上述三个执行完,设为NO时候就是跳过这个查找,然后直接调用5里面对应的未找到方法了。
5.如果还没找到,则如果读值调用valueForUndefinedKey:和如果设值找不到则setValue:forUndefinedKey:方法

代码举例实现原理:

model的.h文件

1#import <Foundation/Foundation.h> 2 3@interface XKCModel : NSObject 4//@property (nonatomic, copy) NSString *name; 5@property (nonatomic, assign) int age; 6 7@end 8 9

model的.m文件

1#import "XKCModel.h" 2 3@implementation XKCModel 4{ 5 NSString *name; //给类中添加成员属性name。通常我们给类添加属性的时候,通过property就已经声明了相对应的set get方法了 6} 7 8//- (void)setName:(NSString *)name { 9// _name = name; 10// NSLog(@"setName"); 11//} 12 13- (void)setAge:(int)age { 14 _age = age; 15} 16 17+ (BOOL)accessInstanceVariablesDirectly { 18 return NO; //默认是YES。此处模拟找不到key的set方法时候,此处又不让继续往下执行查找 属性对象,_属性对象后执行的方法。 19} 20 21- (void)setValue:(id)value forUndefinedKey:(NSString *)key { 22 NSLog(@"出现异常,set时候该key不存在%@",key); 23} 24 25- (id)valueForUndefinedKey:(NSString *)key { 26 NSLog(@"出现异常,get时候该key不存在%@",key); 27 return nil; 28} 29@end 30 31

有关实际调用方法

1- (void)testModelKVC { 2 XKCModel *personModel = [[XKCModel alloc] init]; 3 [personModel setValue:@"肖桑" forKey:@"name"]; 4// [personModel setValue:@"肖桑" forKey:@"namexxxx"]; 5 personModel.age = 11; 6 NSString *KVCName = [personModel valueForKey:@"name"]; 7 8// NSLog(@"%@",personModel.name); 9 NSLog(@"name:%@",KVCName); 10 NSLog(@"age:%d",personModel.age); 11} 12 13

代码举例KeyPath:

1//基类model 2@interface CYXModel: NSObject 3@property (nonatomic, strong) NSString *name; 4@property (nonatomic, strong) CYXShopModel *product; 5@end 6//子model 7@interface CYXShopModel: NSObject 8@property (nonatomic, strong) NSString * productName; 9@end 10 11//设值 12//正常 13CYXModel *model = [[CYXModel alloc]init]; 14model. name = @"CYX"; 15CYXShopModel *shopModel = [[CYXShopModel alloc]init]; 16shopModel. productName = @"NIKE"; 17model. product = shopModel; 18 19//KVC 20CYXModel *model = [[CYXModel alloc]init]; 21NSString *name = [model valueForKey: @"name" ]; 22NSString *productName = [model valueForKeyPath: @"product.productName" ]; 23 24//取值 25//正常 26CYXModel *model = [[CYXModel alloc]init]; 27NSString *name = model. name; 28CYXShopModel *shop = model. product; 29NSString *productName = shop. productName; 30 31//KVC 32CYXModel *model = [[CYXModel alloc]init]; 33NSString *name = [model valueForKey: @"name" ]; 34NSString *productName = [model valueForKeyPath: @"product.productName" ]; 35 36

2. KVO实现原理

在这里插入图片描述
KVO-键值观察机制,原理如下:

  1. 当给A类添加KVO的时候,runtime动态的生成了一个子类NSKVONotifying_A,让A类的isa指针指向NSKVONotifying_A类,重写class方法,隐藏对象真实类信息
  2. 重写监听属性的setter方法,在setter方法内部调用了Foundation 的 _NSSetObjectValueAndNotify 函数
  3. _NSSetObjectValueAndNotify函数内部

a) 首先会调用 willChangeValueForKey
b) 然后给属性赋值
c) 最后调用 didChangeValueForKey
d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

  1. 重写了dealloc做一些 KVO 内存释放

代码如下
person对象监听model中的age变化

监听者

1// 2// KVOobjectTest.m 3// TestBBB 4// 5// Created by MarchWood on 2019/11/19. 6// Copyright © 2019 MarchWood. All rights reserved. 7// 8 9#import "KVOobjectTest.h" 10 11@implementation KVOobjectTest 12 13//@dynamic name; //声明自己接管set get 重写的方法 14//@synthesize name = _name; //自己生成 如果都没写则是默认实现这个的,也就是set getm系统帮你写了 15 16 17- (void)setName:(NSString *)name { 18 _name = [name copy]; 19} 20 21- (void)addObser:(XKCModel *)people { 22 self.tempPeople = people; 23// NSKeyValueObservingOptionInitial 立刻接受回调 24// NSKeyValueObservingOptionNew 值代表时候接受回调 25 [people addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:@"ageChage"]; 26} 27 28- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 29{ 30 if (context == @"ageChage") { 31 XKCModel *model = object; 32 NSLog(@"年龄被改了:%d",model.age); 33 } else { 34 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 35 } 36} 37 38- (void)dealloc 39{ 40 [self.tempPeople removeObserver:self forKeyPath:@"age"]; 41} 42 43@end 44 45

被监听对象

1// 2// XKCModel.m 3// TestBBB 4// 5// Created by MarchWood on 2019/11/19. 6// Copyright © 2019 MarchWood. All rights reserved. 7// 8 9#import "XKCModel.h" 10 11@implementation XKCModel 12 13- (void)setAge:(int)age { 14 15 //可以通过此处进行手动控制通知,但是必须关闭自动通知才可以 16 if (age != _age) { 17 [self willChangeValueForKey:@"age"]; 18 _age = age; 19 [self didChangeValueForKey:@"age"]; 20 } 21} 22 23+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { 24 return NO; //此处设置 25} 26 27//- (void)willChangeValueForKey:(NSString *)key { 28// NSLog(@"Modle 将要改变"); 这个地方这样写会拦截监听方的回调 observeValueForKeyPath 29//} 30// 31//- (void)didChangeValueForKey:(NSString *)key { 32// NSLog(@"Modle 已经改变"); 这个地方这样写会拦截监听方的回调 observeValueForKeyPath 33//} 34 35@end 36 37 38

3. 如何手动触发KVO方法

  • 手动调用willChangeValueForKey和didChangeValueForKey方法
  • 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了 有人可能会问只调用didChangeValueForKey方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey: 记录旧的值,如果不记录旧的值,那就没有改变一说了

4. 通知和代理有什么区别

  • 通知是观察者模式,适合一对多的场景
  • 代理模式适合一对一的反向传值
  • 通知耦合度低,代理的耦合度高

5. block和delegate的区别

  • delegate运行成本低,block的运行成本高

block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。

  • delegate更适用于多个回调方法(3个以上),block则适用于1,2个回调时。

6.为什么Block用copy关键字

Block在没有使用外部变量时,内存存在全局区,然而,当Block在使用外部变量的时候,内存是存在于栈区,当Block copy之后,是存在堆区的。存在于栈区的特点是对象随时有可能被销毁,一旦销毁在调用的时候,就会造成系统的崩溃。所以Block要用copy关键字。

代码交流 2021