面试驱动技术 – KVO && KVC

作者 : 开心源码 本文共8266个字,预计阅读时间需要21分钟 发布时间: 2022-05-12 共182人阅读

面试驱动技术合集(初中级iOS开发),关注仓库,及时获取升级 Interview-series

image

KVO

  • KVO是key-value observing的缩写
  • KVO 是Objective-C对观察者模式的又一实现
  • Apple使用的isa混写(isa-swizzling)来实现KVO

面试题来袭!

友情提醒,智力问答即将开始~

  • addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能取得KVO回调?
/** 增加KVO监听 @param observer 增加观察者,被观察者属性变化通知的目标对象 @param keyPath  监听的属性路径 @param options  监听类型 - options支持按位或者来监听多个事件类型 @param context  监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分 */- (void)addObserver:(NSObject *)observer         forKeyPath:(NSString *)keyPath            options:(NSKeyValueObservingOptions)options            context:(nullable void *)context;
  • 需实现这个方法取得KVO回调
/** 监听器对象的监听回调方法 @param keyPath 监听的属性路径 @param object 被观察者 @param change 监听内容的变化 @param context context为监听上下文,由add方法回传 */- (void)observeValueForKeyPath:(NSString *)keyPath                      ofObject:(id)object                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change                       context:(void *)context;

** 2. apple用什么方式实现对一个对象的KVO?**

  • 答:使用了isa混写技术(isa-swizzling)

** 3. 接着2追问,什么是isa-swizzling?**

以实际开发中,使用KVO的场景分析:

self.person1 = [[MNPerson alloc]init];self.person1.age = 1;NSLog(@"增加KVO之前,person的class是 = %s",object_getClassName(self.person1));[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];NSLog(@"增加KVO之后,person的class是 = %s",object_getClassName(self.person1));--------------------------------------------------------2019-03-04 20:59:24 增加KVO之前,person的class是 = MNPerson2019-03-04 20:59:24 增加KVO之后,person的class是 = NSKVONotifying_MNPerson

what?怎样跑出来一个NSKVONotifying_MNPerson?person的class 不是MNPerson 吗?

image

KVO 原理分析分析

  • 查看 NSKVONotifying_MNPerson 类内部的方法
//打印某个类中的所有方法- (void)printMethonNamesFromClass:(Class)cls{        unsigned int count;    //获取方法列表    Method *methodList = class_copyMethodList(cls, &count);        //保存方法名    NSMutableString *methonNames = @"".mutableCopy;        for (int i = 0; i < count; i++) {                //获取方法        Method method = methodList[i];                NSString *methodName = NSStringFromSelector(method_getName(method));                [methonNames appendFormat:@"%@", [NSString stringWithFormat:@"%@, ",methodName]];            }        NSLog(@"methonNames = %@",methonNames);    //c语音创立的list记得释放    free(methodList);}

结果如下:

 [self printMethonNamesFromClass:object_getClass(self.person1)]; ---------------------------------------------- methonNames = setAge:, class, dealloc, _isKVOA,

画图分析KVO内部结构

imageimage

  • NSKVONotifying_MNPerson 内部为啥要重写setAge:方法呢?

假如自己创立NSKVONotifying_MNPerson对象,会发现KVO直接失效,由于我们自己创立公告了一个NSKVONotifying_MNPerson,导致系统无法动态生成NSKVONotifying_MNPerson这个类,KVO就失效

[general] KVO failed to allocate class pair for name NSKVONotifying_MNPerson, automatic key-value observing will not work for this class

使用- (IMP)methodForSelector:(SEL)aSelector函数,获取实际的方法实现!

image

- (void)viewDidLoad {    [super viewDidLoad];     self.person1 = [[MNPerson alloc]init];    self.person2 = [[MNPerson alloc]init];    NSLog(@"增加KVO之前,person1的setAge是 = %p,person2的setAge是 = %p",          [self.person1 methodForSelector:@selector(setAge:)],          [self.person2 methodForSelector:@selector(setAge:)]);        [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];        NSLog(@"增加KVO之后,person1的setAge是 = %p,person2的setAge是 = %p",          [self.person1 methodForSelector:@selector(setAge:)],          [self.person2 methodForSelector:@selector(setAge:)]);    }

使用 p(IMP) + 函数地址,可以查看方法实现!这里可以看到,增加kvo之后,setAge: 被重写了,变成了_NSSetLongLongValueAndNotify方法

(lldb) p (IMP)0x10c1107d0(IMP) $0 = 0x000000010c1107d0 (KVO-Demo`-[MNPerson setAge:] at MNPerson.h:13)(lldb) p (IMP)0x10c456bf4(IMP) $1 = 0x000000010c456bf4 (Foundation`_NSSetLongLongValueAndNotify)

和属性的类型有关,假如age 是 int 类型,重写的setAge:方法,就是调用 _NSSetIntValueAndNotify

  • 使用以下指令,查找Foundation中包含ValueAndNotify的方法
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify

image

可以看到各种类型的_NSSetXXXValueAndNotify

  • 可以看到各种类型的_NSSetXXXValueAndNotify内部实现的探索

由于我们不能手动创立NSKVONotifying_MNPerson类,为了窥探_NSSetXXXValueAndNotify内部的实现咋办? => 在NSKVONotifying_MNPerson的父类 – MNPerson里面窥探,(子类会调用父类的super方法

//伪代码@implementation NSKVONotifying_MNPerson- (void)setAge:(NSInteger)age{        _NSSetLongLongValueAndNotify();}void _NSSetLongLongValueAndNotify(){        [self willChangeValueForKey:@"age"];    [super setAge:age];    [self didChangeValueForKey:@"age"];}- (void)didChangeValueForKey:(NSString *)key{        //通知监听器,属性值发生了改变    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];}@end
  • 验证
- (void)setAge:(NSInteger)age{    NSLog(@"setAge:");    _age = age;}- (void)willChangeValueForKey:(NSString *)key{    [super willChangeValueForKey:key];    NSLog(@"willChangeValueForKey");}- (void)didChangeValueForKey:(NSString *)key{    NSLog(@"didChangeValueForKey - begin");    [super didChangeValueForKey:key];    NSLog(@"didChangeValueForKey - end");}@end-----------------------------------------------------------------2019-03-04 21:53:46.574543+0800 KVO-Demo[55867:7772356] willChangeValueForKey2019-03-04 21:53:46.575037+0800 KVO-Demo[55867:7772356] setAge:2019-03-04 21:53:46.575518+0800 KVO-Demo[55867:7772356] didChangeValueForKey - begin2019-03-04 21:53:46.575822+0800 KVO-Demo[55867:7772356] <MNPerson: 0x60000001aa00>对象的age属性改变了 = {    kind = 1;    new = 2;    old = 0;}2019-03-04 21:53:46.576014+0800 KVO-Demo[55867:7772356] didChangeValueForKey - end
  • 答复:什么是isa混写
  1. 利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,并且让当前的instance对象的isa指向这个全新子类
  2. 当修改 instance对象的属性时,会触发set方法,调用Foundation的 _NSSetXXXValueAndnotify函数
    • willChangeValueForKey:
    • [super set:](父类原来的setter方法)
    • didChangeValueForKey
    • 内部触发监听器(Oberser)的监听方法 – observeValueForKeyPath: ofObject: change: context:

RuntimeAPI : objc_allocateClassPairobjc_registerClassPair.动态生成 NSKVONotifying_XXX


NSKVONotifying_X 类重写class方法

2019-03-04 22:00:34 增加KVO之前, [self.person2 class] = MNPerson,object_getClass(self.person1) = MNPerson2019-03-04 22:00:38 增加KVO之后, [self.person2 class] = MNPerson,object_getClass(self.person1) = NSKVONotifying_MNPerson

由上代码发现 ==> NSKVONotifying_MNPerson 重写了class 方法,假如通过 object_getClass 得到实际的isa指向的话,发现其真实的类是NSKVONotifying_MNPerson,那么问题来了,为啥苹果要重写class方法呢?

  • NSKVONotifying_MNPerson,重写class方法的起因

Key-Value Observing Programming Guide 对 KVO的形容:

Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

人工智能翻译:使用称为isa-swizzling的技术实现自动键值观察…当观察者注册对象的属性时,观察对象的isa指针被修改,指向中间类而不是真正的类,让开发者只关心他需要关心的类(那些他自己创立出来的类)

人工智障解读:由于他不想公开这个类,从开发者的角度来看,NSKVONotifying_MNPerson并不是客户创立的,屏蔽内部实现,隐藏NSKVONotifying_MNPerson

猜测 NSKVONotifying_MNPerson 内部实现

@implementation NSKVONotifying_MNPerson- (Class)class{    return [MNPerson class];}@END

不重写的话

@implementation NSObject- (Class)class{    return object_getClassName(self);}@end

不重写的情况下,使用 [person class] 真实的类就暴露出来了NSKVONotifying_MNPerson,这是苹果所不希望看到的

  • 如何手动触发一个value的KVO?

手动调用

  • willChangeValueForKey:
  • didChangeValueForKey:

老实说,这种一般也只会存在于面试题中,正常开发中基本上不会存在,拿来应付面试足矣~

image

  • 直接修改成员变量会触发KVO吗?
@interface MNPerson : NSObject{    //将成员变量暴露出来    @public NSInteger _age;}@property (nonatomic, assign) NSInteger age;@property (nonatomic, strong) MNCar *car;@end------------------------------------------------调用- (void)viewDidLoad {    [super viewDidLoad];        self.person1 = [[MNPerson alloc]init];    [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];    //直接修改成员变量    self.person1->_age = 20;}
  • 答:直接修改成员变量不会触发KVO – 没有调用Setter方法,除非手动触发KVO

KVC

Key-Value Coding – 键值编码

KVO 常用方法

- (void)setValue:(nullable id)value forKey:(NSString *)key;就不说了,就简单的设置对象的属性值;

KVC和KVO的keyPath肯定是属性么

  • KVC 是可以直接设置成员变量的
  • KVO 必需手动实现 成员变量的监听

讲一下setValue:forKeyPath: 的作用

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

forKeyPath – 路径,相似节点,

- (void)viewDidLoad {    [super viewDidLoad];        self.person1 = [[MNPerson alloc]init];    self.person1.car = [[MNCar alloc]init];    [self.person1 setValue:@"testCar" forKeyPath:@"car.name"];        NSLog(@"carname = %@",self.person1.car.name);}--------------------------------------------------------2019-03-04 22:37:26 carname = testCar
  • 使用KVC,能否会破坏面向对象的编程方法(有违反于面向对象的编程思想)?
  • 其实是会的,KVC 可以直接获取、修改类不想暴露的私有变量,所以会破坏面向对象的编程思想
  • TextView 设置placeholder的可以用到

KVC修改属性能否会触发KVO

答:会触发KVO

WHY? (内心毫无波动,甚至有的想打代码)

imageimage

所以 – 假如没有set方法,KVC 也不肯定会报错!

image

//是否直接访问成员变量,默认YES+ (BOOL)accessInstanceVariablesDirectly{        return YES;}

老实说,见过有面试题问查找顺序的,假如说成员变量查找,比方属性name公告,会自动生成一个_name,优先查找还能了解,问之后的什么_isKey,key 的顺序的,个人感觉完全毫无意义啊,并不能仅由于这个顺序,就断定面试者的水平啊,由于正常开发中,总不能有人写个 _name,又写个isName,再写个_isName,而后来个你画我猜,看看哪个顺序,这脑瓜子预计得被人打放屁了都。其实这种大致能答复出流程就行了,KVO && KVC 其实考的一般也就到这,要问深度的话,完全可以在其余领域,比方runtime 、 runLoop之类的话题上深入,没必要纠结具体内部成员变量的查找顺序之类的(个人愚见,不喜请喷)

KVO & KVC 的常见考题应该大致逃不出这些了,其实KVO & KVC 在考题上挺常见了,也算是高频考点了,但是感觉相对来说,题目还是偏初中级。之前有略微搜下了少量这个话题相似的文字,发现都大同小异,由于一般的技术点也差不多这些,原本在犹豫这篇文章能否要发,后来由于是想做一个面试知识体系系列 (面试驱动技术合集) ,还是丢出来,如有雷同,纯属KVO & KVC 太常见了~请见谅


友情演出:小马哥MJ

参考资料

Key-Value Observing Programming Guide

isa混写探索

招聘一个靠谱的 iOS

ChenYilong/iOSInterviewQuestions

手动设定实例变量的KVO实现监听

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 面试驱动技术 – KVO && KVC

发表回复