iOS KVO/KVC

前言

这篇博客由两个问题引出

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

KVO的使用

@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

比如我们有一个类 其中有一个age属性
我们想要监听age属性的改变 从0到1的那种

    self.person = [[Person alloc] init];
    self.person.age = 10;
    
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.age = 20;
}

摸它一下它就会变了

呜呜好想知道你什么时候变的喔

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"age" options:options context:@"1"];

给它加上一个监视它的小东C 这个小东C在这个属性变化的时候就会告诉你
内鬼找到了家人们

之后怎么给你发消息呢 需要你实现一个方法 (给你当内鬼不得给点儿好处)

 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    //当监听对象的属性值发生改变时 调用方法
    NSLog(@"%@ of %@ change %@", keyPath, object, change);
}
  • keyPath : 监听的对象是谁
  • object : 监听的对象属于谁 (不要以为你是痴汉监听了人家 人家就属于你哦 不一定的喔)
  • change : 从前面的定义可以看出 是一个字典 存放了new值和old值
  • context : 相当于是一个标记 跟前面添加监听的时候对应起来

当然了 记得dealloc 谁都不想让内鬼被发现的喔

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

KVO本质分析

比如我们有两个对象

    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"person1"];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person1.age = 21;
    self.person2.age = 22;
}

只给person1添加了监听 为什么person1.age可以走到- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法呢
聪明的小伙伴要说啦 因为给它加了监听呀 person2.age没加呀

是对的 但是只是表象喔

分别打印person1person2isa指针、
在这里插入图片描述
person1的类变成了NSKVONotifying_Person

注意不能打印person1.age->isa
在这里插入图片描述
懂我意思吧

未使用KVO监听的对象

Person类的实例对象里面存放了一个isa指针
isa指针指向了Person类的类对象 类对象里面存放了isa指针 superclass指针
实例方法比如setter getter方法等
当然了 Person类的isa指针指向了Person的元类对象

在我们调用self.person2.age = 22的时候 通过实例对象的isa指针寻找Person类中的setter方法 然后调用

- (void)setAge:(int)age {
	_age = age;
};

这是正常的对象 没有使用KVO监听的

使用了KVO监听的对象

Runtime动态创建的一个类 叫NSKVONotifying_Person 它是Person类的一个子类

也就是Person类的实例对象的isa指针指向了NSKVONotifying_Person类对象

说的高级一点
使用了KVO监听的实例对象的isa指针会指向一个动态创建的一个全新的类
这个类对象里面也会有isa指针 superclass指针 实例方法等等 并且是以前定义的类的子类

NSKVONotifying_Person类对象的superclass指针指向了Person类对象

而代码执行到self.person1.age = 22的时候 实例对象的isa指针寻找到了NSKVONotifying_Person类中的setAge方法

而这个setAge方法跟Person类中的不太一样哦

- (void)setAge:(int)age {
    _NSSetIntValueAndNotify();
}

酱紫的

这就导致了没有监听和监听了的 调用的setter方法是不一样的 调用别的方法也是一个原理

大概就是这样的 大概喔大概 网上都这么说的 伪代码 流程差不多就是这样的

- (void)setAge:(int)age {
    _NSSetIntValueAndNotify(); //这里监听的是int类型 
    //_NSSetXXXValueAndNotify()
}
void NSSetIntValueAndNotify() { 
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

在这里插入图片描述

本质分析验证

加入两行代码 调用object_getClass函数验证一下子
在这里插入图片描述
通过重写上述_NSSetIntValueAndNotify();伪代码中的两个方法进行验证
查看调用顺序是否正确

- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"will change Value For Key Begin");
    [super willChangeValueForKey:key];
    NSLog(@"will change Value For Key end");
}
- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"did change Value For Key Begin");
    [super didChangeValueForKey:key];
    NSLog(@"did change Value For Key End");
}

在这里插入图片描述
逻辑清晰 条理清楚嗷家人们
didChangeValueForKey调用了监听器的方法

验证差不多了

子类的内部方法

有哪些

我们写个函数 得到一下类内部的方法列表

- (void)printMethodNamesOfClass:(Class)cls {
    
    unsigned int count;
    //获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    for (int i = 0; i < count; i++) {
        //获得方法
        Method method = methodList[i];
        
        //获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    free(methodList);
    
    NSLog(@"%@ - %@", cls, methodNames);
}

然后在self.person1加入监听了之后调用一下

    [self printMethodNamesOfClass:object_getClass(self.person1)];
    [self printMethodNamesOfClass:object_getClass(self.person2)];

在这里插入图片描述
这样我们就得到了类对象的内部方法列表

class

通过[self.person1 class]也可以得到类对象 上面验证的代码中 在添加了监听之后
通过runtime函数object_getClass得到的类对象是先前说过的动态创建的类对象NSKVONotifying_Person
而调用[self.person1 class]得到的是什么呢

NSLog(@"%@ -- %@", [self.person1 class], [self.person2 class]);

在这里插入图片描述
跟我们想的不太一样? 或者说 跟我写的不太一样
怎么事儿呢

所以NSKVONotifying_Person 类中重写了class方法

- (Class)class {
	return [Person class];
}

在我们调用[self.person1 class]的时候 通过self.person1isa指针 找到了NSKVONotifying_Person 类对象 在类对象中找到了class类方法 最终调用 返回了Person class

那为什么要重写class方法呢

因为苹果需要屏蔽内部实现 隐藏了这个类的存在 不希望开发者多想
如果一定要知道底层的实现 就使用runtime函数咯

小总结

就是因为一个加了监听一个没加监听
导致了他们两个的isa指针不一样 导致了他们找到的类对象不一样
也就导致了最后方法的实现不一样

前言中的关于KVO两个问题的解答

iOS用什么方式实现对一个对象的KVO? (KVO的本质是什么)

  • 利用RuntimeAPI动态生成一个子类 并且让instance对象的isa指针指向这个全新的子类
  • 当修改instance对象的属性时 会调用Foundation的_NSSetXXXValueAndNotify函数
  • willChangeValueForKey
  • 父类原来的setter
  • didChangeValueForKey
  • 内部会触发监听器的监听方法observeValueForKeyPath: ofObject: change: context:

如何手动触发KVO

  • 手动调用willChangeValueForKeydidChangeValueForKey

这个有个注意点 因为我加在之前那个触发改变属性值的地方之后 思索了一下好像没什么屁用啊
在这里插入图片描述
后来我才想明白
在这里插入图片描述
这两句代码可以加在任何地方 都会直接触发- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法

可能是我自己没想明白 大家应该不会有这样的疑问

直接修改成员变量会触发KVO吗

肯定是不会的
直接修改之后如果加上手动触发的方法 就可以触发

另一个前言

关于KVC也有两个问题需要解答

  • 通过KVC修改属性会触发KVO吗
  • KVC的赋值和取值过程是怎样的 原理是什么

KVC的使用

首先是常用的KVC的API

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

前面两个是用来设置属性值的
后面两个是用来获取属性值的

通过KVC修改属性会触发KVO吗

KVC的赋值和取值过程是怎样的 原理是什么

赋值

通过传进来的key 寻找setKey:方法 如果没有 就寻找_setKey:方法
哪怕根本没有key这个成员变量

    [self.person1 setValue:@10 forKey:@"age"];

在类的定义和实现文件中

#import <Foundation/Foundation.h>

@interface Person : NSObject
//@property (nonatomic, assign) int age;
@end
#import "Person.h"

@implementation Person

- (void)setAge:(int)age {
    NSLog(@"setAge = %d", age);
}
@end

可以看到我们已经注释了age属性 但是重写了setAge方法
验证一下
在这里插入图片描述

如果没有setKey方法和_setKey方法的话
会调用

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

返回值的意思是是否允许直接访问成员变量

如果是NO 那setValue:forKey方法会直接报错

如果是YES 会根据_key _isKey key isKey的顺序来访问成员变量

找到就修改 没找到就报错咯

取值

[self.person1 valueForKey:@"age"];
- (int)getAge {
    return 1;
}
- (int)age {
    return 2;
}
- (int)isAge {
    return 3;
}
- (int)_age {
    return 4;
}

查找的时候会按照这四个方法的顺序查找

都没找到

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

查看这个方法的返回值 默认是YES
如果是NO的话 报错

如果是YES
根据_key _isKey key isKey的顺序来访问成员变量 获得成员变量的值

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

waxuuuu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值