【iOS】—— KVO与KVC

1. KVO

首先需要了解KVO基本使用,KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

 #import "ViewController.h"
#import "Person.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    
    [p1 addObserver:self  forKeyPath:@"age"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
    
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
    NSLog(@"%@", change[@"old"]);
    NSLog(@"%@", change[@"new"]);
    
}
@end

在这里插入图片描述

KVO底层实现分析

KVO 的底层实现利用了 Objective-C 的 Runtime 特性,动态创建子类并重写属性的 setter 方法,以实现属性变化的观察和通知机制。 在这里插入图片描述
通过上面我们可以发现,再对象p1对象执行addObserver操作之后,p1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifyin_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。

未使用KVO监听的对象放大实现路径

上图是p1没有执行addObserver操作之前isa指针的实际指向。但是,在p1添加addObserver操作之后,p1对象的isa指针就如上面所示指向为NSKVONotifying_Person,NSKVONotifying_Person是Person的子类,也就是说其superclass指针是指向Person类对象的,NSKVONotifyin_Person是runtime在运行时生成的。那么p1对象在调用setage方法的时候,肯定会根据p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及实现。

如何验证上面的说法:
 NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
 [p1 addObserver:self  forKeyPath:@"age"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
  NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    

在这里插入图片描述
我们可以发现:**添加KVO监听之前,p1和p2的setAge方法实现的地址相同,而经过KVO监听之后,p1的setAge方法实现的地址发生了变化,**我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1的setAge方法的实现由Person类方法中的setAge方法转换为了C语言的Foundation框架的_NSsetIntValueAndNotify函数。

NSKVONotifyin_Person内部结构

首先我们清楚NSKVONotifyin_Person,作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部一定对setAge方法做了单独的实现,

 - (void)viewDidLoad {
    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];
    p1.age = 1.0;
    Person *p2 = [[Person alloc] init];
    p1.age = 2.0;
    // self 监听 p1的 age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

    [self printMethods: object_getClass(p2)];
    [self printMethods: object_getClass(p1)];

    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void) printMethods:(Class)cls
{
    unsigned int count ;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString: methodName];
        [methodNames appendString:@" "];
        
    }
    
    NSLog(@"%@",methodNames);
    free(methods);
}

运行结果如下:
在这里插入图片描述
通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。

NSKVONotifyin_Person的内存结构以及方法调用顺序

**这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。**不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。

如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObject的class的实现大致为返回自己isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,apple不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class类,直接返回Person类。

didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

在Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟他们的实现。

- (void)setAge:(int)age {
    NSLog(@"setage:");
    _age = age;
    
}

- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}

运行结果:
在这里插入图片描述

回答问题:

  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 答. 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。
  1. 如何手动触发KVO? 答. 被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

2. KVC

简介:

KVC的全称是KeyValueCoding,俗称“键值编码”,可以通过一个key来访问某个属性;

KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量;

它是一个非正式的Protocol,提供一种机制来间接访问对象的属性,而不是通过调用Setter、Getter方法访问。KVO 就是基于 KVC 实现的关键技术之一。

常见的API:

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

key和keyPath的区别

  • key:只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的。
  • keypath:除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链。
key:
Person* person = [[Person alloc] init];
[person setValue:@"I am Father" forKey:@"name"];
NSLog(@"%@", [person valueForKey:@"name"]);

输出结果:
在这里插入图片描述

keyPath:
person.son = [[PersonSon alloc] init];
[person setValue:@"I am Son" forKeyPath:@"son.sonName"];
NSLog(@"%@", [person.son valueForKey:@"sonName"]);

输出结果:
在这里插入图片描述

批量重复操作

 Person* personFirst = [[Person alloc] init];
 [personFirst setValue:@"lcy" forKey:@"name"];
 [personFirst setValue:@"20" forKey:@"age"];
 [personFirst setValue:@"男" forKey:@"sex"];
 NSLog(@"name = %@, age = %ld, sex = %@",personFirst.name, (long)personFirst.age, personFirst.sex);

 NSDictionary* dictionary1 = [personFirst dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
 NSLog(@"dictionary1 = %@", dictionary1);


 NSDictionary* dictioinary2 = @{@"name": @"lyt", @"age": @11, @"sex": @"男"};
 Person* personSecond = [[Person alloc] init];
 [personSecond setValuesForKeysWithDictionary:dictioinary2];
 NSLog(@"name = %@, age = %ld, sex = %@",personSecond.name, (long)personSecond.age, personSecond.sex);

输出结果:
在这里插入图片描述

字典模型相互转化

如果model属性和dic不匹配,可以重写方法-(void)setValue:(id)value forUndefinedKey:(NSString *)key

*重点:-(void)setValue:(id)value forUndefinedKey:(NSString )key 方法在函数中有定义,但是没有实现需要自己来实现,从而供后面来调用。如果自己不重写的话,遇到Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface StudentModel : NSObject

@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSString* age;
@property (nonatomic, strong) NSString* studentSex;

@end

NS_ASSUME_NONNULL_END

#import "StudentModel.h"

@implementation StudentModel
- (void) setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"sex"]) {
        self.studentSex = (NSString*) value;
    }
}
@end

//main函数
NSDictionary* dictionary = @{@"name": @"stu1", @"age": @66, @"sex": @"nv"};
StudentModel* model = [[StudentModel alloc] init];
[model setValuesForKeysWithDictionary:dictionary];
NSLog(@"model.name: %@", model.name);
NSLog(@"model.age: %@", model.age);
NSLog(@"model.sex: %@", model.studentSex);

NSDictionary* tempdict = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
NSLog(@"tempdict = %@", tempdict);

输出结果:

在这里插入图片描述

KVC原理

赋值原理

在日常开发中,针对对象属性的赋值,一般有以下两种方式:

  • 直接通过setter方法赋值;
  • 通过KVC键值编码的相关API赋值;

当调用setValue:forKey:设置属性value时,其底层的执行流程为:

  1. 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set<Key>:-> _set<Key> -> setIs<Key>
    • 如果有其中任意一个setter方法,则直接设置属性的value(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)
    • 如果都没有,则进入【第二步】
  2. 【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly是否返回YES,
    • 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_<key> -> _is<Key> -> <key> -> is<Key>
      • 如果找到其中任意一个实例变量,则赋值。
      • 如果都没有,则进入【第三步】。
    • 如果返回NO,则进入【第三步】。
  3. 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。
    综上所述,KVC通过 setValue:forKey: 方法设值的流程以设置LGPerson的对象person的属性name为例,如下图所示:

setValue:forKey:的原理

取值原理
  1. kvc取值按照 getKey、key、iskey、_key 顺序查找方法。
  2. 存在直接调用。
  3. 没找到同样,先查看accessInstanceVariablesDirectly方法。
+ (BOOL)accessInstanceVariablesDirectly{
      return YES;   ///> 可以直接访问成员变量
  //    return NO;  ///>  不可以直接访问成员变量,  
  ///> 直接访问会报NSUnkonwKeyException错误  
 }
  1. 如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量。
  2. 找到直接复制。
  3. 未找到报错NSUnkonwKeyException错误。

valueForKey:的原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值