iOS基本功iOS面试总结将来跳槽用

iOS键值编码(KVC)与键值监听(KVO)、

2019-03-13  本文已影响70人  晴天ccc

描述

KVO全称KeyValueObserving。俗称“键值监听”。
利用Key来找到某个对象并监听其属性的改变。也是一种典型的观察者模式。
在某个对象注册监听者后,被监听对象的属性发生改变时,会发送一个通知给监听者。以便监听者执行回调操作。

本文演示代码地址

KVO方法介绍

1、通过addObserver:forKeyPath:options:context:方法注册观察者。

/**
 添加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;

2、通过 observeValueForKeyPath:ofObject:change:context:获得回调,从而做出事件处理。

/**
 监听器对象的监听回调方法

 @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;

3、当观察者不需要监听时,调用可以removeObserver:forKeyPath:方法将KVO移除。需要注意的是:调用removeObserver需要在观察者消失之前,否则会导致Crash。

- (void)dealloc{
    [self removeObserver:self forKeyPath:@"keyFlag"];
}

简单示例

@interface ViewController ()

@property (nonatomic,strong) Animal * ani;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.ani = [[Animal alloc] init];

    self.ani.age = 10;
    
  // 添加键值监听
    [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    

}

// 点击事件,触发属性修改
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.ani.age += 5;
}

// 获得回调,实时监听属性改变、
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
}

// 需要在不使用的时候,移除监听
- (void)dealloc{
    [self.ani removeObserver:self forKeyPath:@"age"];
}

@end

KVO原理探究

1、利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,并且让当前instance对象isa指针指向这个全新子类。
2、当修改instance对象的属性时,会触发setter方法,调用Foundation的_NSSetXXXValueAndnotify函数

  • 调用willChangeValueForKey:
  • 调用原来的setter实现(父类原来的setter方法)
  • 调用didChangeValueForKey
    此时内部触发监听器(Oberser)的监听方法 - observeValueForKeyPath: ofObject: change: context:

代码验证上述流程

第一步:通过runtime查看isa指针指向的 class对象

如果观察 Animal的age属性。
系统会在运行时生成NSKVONotifying_Animal
在NSKVONotifying_Animal中重写setterclassdealloc等方法。
使Animal实例对象的isa指针指向NSKVONotifying_Animal
NSKVONotifying_Animal的superclass指向Animal

探究过程

    // 注册成为观察者
    NSLog(@"添加KVO之前,Animal的class是 = %s",object_getClassName(self.ani));
    [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    NSLog(@"添加KVO之后,Animal的class是 = %s",object_getClassName(self.ani));

结果如下:

 添加KVO之前,Animal的class是 = Animal
 添加KVO之后,Animal的class是 = NSKVONotifying_Animal

注册成为观察者之后,类变成了NSKVONotifying_Animal而再是 Animal
我们先看一下NSKVONotifying_Animal类内部的方法。

 #import <objc/runtime.h>

//打印某个类中的所有方法
- (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.ani)];
 ----------------------------------------------
 methonNames = setAge:, class, dealloc, _isKVOA,
画图分析KVO内部结构

第二步:- (void)setAge:(int)age方法

为了比较在注册观察者前后setter方法的变化,我们新创建一个实例ani1

    self.ani = [[Animal alloc] init];
    self.ani1 = [[Animal alloc] init];

    NSLog(@"添加KVO之前,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
          [self.ani methodForSelector:@selector(setAge:)],
          [self.ani1 methodForSelector:@selector(setAge:)]);

    // 注册成为观察者
    [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    
    NSLog(@"添加KVO之后,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
          [self.ani methodForSelector:@selector(setAge:)],
          [self.ani1 methodForSelector:@selector(setAge:)]);

结果如下:

添加KVO之前,ani的setAge是 = 0x10d751460,未添加KVO的ani1的setAge是 = 0x10d751460
添加KVO之后,ani的setAge是 = 0x10daaacf2,未添加KVO的ani1的setAge是 = 0x10d751460

这里可以看到,添加KVO前后,setAge方法有所改变
我们进入debugger来看看这第这个方法的实现到底是怎样的:

(gdb) print (IMP) 0x10daaacf2 
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify> 

原来在重写的NSKVONotifying_Animal-setAge方法中会调用_NSSetIntValueAndNotify:

// 注:Foundation框架中类似_NSSetIntValueAndNotify的方法实现还有很多:
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSet0bjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
查看_NSSet*ValueAndNotify的内部实现
- (void)setAge:(int)age{
    _NSSet*ValueAndNotify();
}

// 因为_NSSetIntValueAndNotify在Foundation框架中,无法查看起具体实现,根据实践猜测大致为代码如下:
void _NSSet*ValueAndNotify()
{
      [self willChangeValueForKey:@"age"];
      [super setAge:age];
      [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
          //通过监听器,监听属性发生了改变
  [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
在Animal类中验证
#import "Animal.h"

@implementation Animal

//Animal内部代码实现
- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey == begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey == end");
}
@end

结果如下:

kvoAndkvoDemo[2391:50226] willChangeValueForKey
kvoAndkvoDemo[2391:50226] setAge
kvoAndkvoDemo[2391:50226] didChangeValueForKey == begin
kvoAndkvoDemo[2391:50226] 监听到了<Animal: 0x600000da3690>对象的age属性由{
    kind = 1;
    new = 15;
    old = 10;
}变成了{
    kind = 1;
    new = 15;
    old = 10;
}属性
kvoAndkvoDemo[2391:50226] didChangeValueForKey == end

如果不添加监听,则不会执行willChangeValueForKeydidChangeValueForKey方法、验证成功!

汇总

1、当你观察一个对象时,系统通过Runtime动态的创建一个该类的派生类,这个类继承自该对象的原本的类,并了重写被观察属性的setter方法。
2、isa指针会指向这个新创建的类,该对象就变成新创建子类的实例了、
3、重写的setter方法,执行_NSSet*ValueAndNotify,会负责在调用原来的setter方法前后,通知所有观察对象:值的改变。

拓展思考

1、用法听起来和NSNotification很相似啊, 其实NSNotification也是观察者模式,但是NSNotification是一种广播机制,KVO是被观察者直接发消息给观察者,是对象间的相互沟通。NSNotification则是两者都和通知中心对象交互,对象之间不知道彼此。
2、KVO行为是同步的,并且发生与观察的值发生在同样的线程上,没有队列或Run-Loop处理。【使用注意】

用途

常见运用是监听ScrollViewcontentOffset属性。当用户滚动结束时动态改变某些空间的实现效果。下拉刷新,渐变导航栏,头像变大缩小等。


KVC

KVC (Key-Value-Coding )键值编码。顾名思义:可以通过一个Key来访问某个属性。

常用方法
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

- (id)valueForKey:(NSString *)key; 
- (id)valueForKeyPath:(NSString *)    keyPath;
简单示例
    self.ani = [[Animal alloc] init];
    [self.ani setValue:@38 forKey:@"age"];
    NSLog(@"%d",self.ani.age);
    
    self.ani1 = [[Animal alloc] init];
    [self.ani1 setValue:@99 forKeyPath:@"cat.weight"];
    NSLog(@"%d",self.ani1.cat.weight);

    self.ani2 = [[Animal alloc] init];
    self.ani2.age = 10;
    NSLog(@"%@",[self.ani2 valueForKey:@"age"]);
setValue:forKey:的原理:

当我们设置setValue:forKey:时
首先会查找setKey:、_setKey: (按顺序查找)
如果有直接调用
如果没有,先查看accessInstanceVariablesDirectly方法
如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量
找到直接复制
未找到报错NSUnkonwKeyException错误

valueForKey:的原理:

kvc取值按照 getKey、key、iskey、_key 顺序查找方法
存在直接调用
没找到同样,先查看accessInstanceVariablesDirectly方法
如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量
找到直接复制
未找到报错NSUnkonwKeyException错误

思考

我们可以通过 self.ani.age = 10; 来赋值,也可通过上述代码进行赋值,看着多此一举、
可是如果人这个类的属性是没有暴露在外面呢?比如现在给人这个类一个私有的身高的属性。就可以通过KVC进行赋值、

Key 和 KeyPath区别接联系

Key:只能访问当前对象的属性,如果按路径找会报错。
KeyPath:相当于根据路径去寻找属性,能利用运算符一层一层往内部访问属性。

用途

我们通过KVC可以直接对私有属性并进行赋值
字典转模型

拓展

我们通过XIB或者SB拖线布局连线错误的时候也会报错说找不到什么key,说明Storyboard在赋值的时候也是通过KVC来操作的。

试题

KVO相关:
1. iOS用什么方式来实现对一个对象的KVO?(KVO的本质是什么?)
2. 如何手动出发KVO?
3. 直接修改成员变量会触发KVO么?

KVC相关: 
1. 通过KVC修改属性会触发KVO么?
2. KVC的赋值和取值过程是怎样的?原理是什么?
上一篇下一篇

猜你喜欢

热点阅读