面试驱动技术 - KVO && KVC

面试驱动技术合集(初中级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是 = 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实现监听

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 作者:wangzz原文地址:http://blog.csdn.net/wzzvictory/article/det...
    反调唱唱阅读 1,114评论 0 5
  • KVC/KVO 概念 KVC : 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准...
    满脸胡茬的小码农阅读 1,948评论 2 8
  • 在磁器口古街有一家卖印度飞饼的,两个印度人操着汉语喊着“来来来,印度飞饼”,我很好奇,给他们录像,问他们真的是印度...
    雷春阅读 1,083评论 9 24