面试驱动技术 - KVO && KVC

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是 = MNPerson
2019-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内部结构

image

image

  • 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] willChangeValueForKey
2019-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 - begin
2019-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) = MNPerson
2019-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? (内心毫无波动,甚至有的想打代码)

image

image

所以 - 如果没有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实现监听

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券