iOS KVO

2021-01-26  本文已影响0人  陈盼同学

https://opensource.apple.com/tarballs/objc4/

由 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 这题展开了论证

简单概括为
当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类
子类拥有自己的set方法实现,内部会调用
willChangeValueForKey:
原来的setter
didChangeValueForKey:,这个方法内部又会调用监听器(observer)的监听方法

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。KVO是观察者模式的又一实现。

普通KVO使用

//0首先新建一个MJPerson类,类里有一个age属性。
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property (assign, nonatomic) int age;
@end

#import "MJPerson.h"
@implementation MJPerson
@end

//1.然后在ViewController里初始化MJPerson类
MJPerson *p1 = [[MJPerson alloc] init];
p1.age = 1;
MJPerson *p2 = [[MJPerson alloc] init];

//2.self监听p1的age属性  context主要用来传参,会在监听方法里收到
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
p1.age = 2;
p2.age = 3;

//3.用完需要移除(在这里p1是个局部,注意移除放置位置)
[p1 removeObserver:self forKeyPath:@"age"];

//4.监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@改变了 - %@", object, keyPath, change);
}

疑问,上述里的

p1.age = 1; //这句话调用了p1的setAge方法
p1.age = 2; //这句话也只是调用了p1的setAge方法,但是为什么这句话打印了监听方法,上句话就没有打印监听方法呢,可能是运行的时候被动了手脚

解惑过程

在添加KVO监听处打断点调试

(lldb) po p1->isa
MJPerson
(lldb) po p2->isa
MJPerson

在添加KVO监听后打断点调试

(lldb) po p1->isa
NSKVONotifying_MJPerson
(lldb) po p2->isa
MJPerson

通过打印p1的isa指针(日志栏输入 po p1->isa),可以发现p1.age = 1时isa指针指向MJPerson;添加了kvo后,在p1.age = 2断点时,isa指针指向NSKVONotifying_MJPerson

通过打印isa指针可以看出添加KVO之后指向了新的子类,现在通过运行时的方法object_getClass也可以证明。

导入#import <objc/runtime.h>
然后
NSLog(@"添加KVO之前 p1 = %@ , p2 = %@ ",object_getClass(p1),object_getClass(p2));
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后 p1 = %@ , p2 = %@ ",object_getClass(p1),object_getClass(p2));
可以看到日志栏输出
2019-11-21 13:08:32.443102+0800 Test[4464:117233] 添加KVO之前 p1 = MJPerson , p2 = MJPerson
2019-11-21 13:08:32.443342+0800 Test[4464:117233] 添加KVO之后 p1 = NSKVONotifying_MJPerson , p2 = MJPerson

那么NSKVONotifying_MJPerson与MJPerson有什么关系呢?
其实,NSKVONotifying_MJPerson是MJPerson的子类,添加kvo后,p1实例对象的isa指针指向了NSKVONotifying_MJPerson类对象,NSKVONotifying_MJPerson类对象的superclass指针指向了MJPerson类对象。

MJPerson类对象里的类结构是

isa
superclass
setAge:
age
...

NSKVONotifying_MJPerson类对象里的类结构是

isa
superclass
setAge:
class
dealloc
isKVOA
......

在调用NSKVONotifying_MJPerson类对象的setAge时,本质是调用了Foundation的_NSSetValueAndNotify方法(这是个私有函数)
我们可以新建一个NSKVONotifying_MJPerson继承MJPerson,在NSKVONotifying_MJPerson里实现_NSSet
ValueAndNotify过程的伪代码如下

- (void)setAge:(int)age
{
    __NSSet*ValueAndNotify();
}

void __NSSet*ValueAndNotify()  //c语言函数
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    [observer observeValueForKeyPath:@"age" ofObject:self change:@{} context:nil];//didChangeValueForKey又会调用kvo的监听方法
}

所以整体调用流程是给一个属性实现监听后,isa指针会改变指向,指向了当前类的新生成的一个子类,新生成的子类会重写setAge方法,调用新生成的子类的setAge方法,setAge方法里又会调用c语言的_NSSetValueAndNotify方法,_NSSetValueAndNotify方法里又做了三件事情,[self willChangeValueForKey:@"age"];接下来调用父类的[super setAge:age];,然后[self didChangeValueForKey:@"age"]; ,[self didChangeValueForKey:@"age"]这个方法里又会调用监听器的监听方法- observeValueForKeyPath方法,最后来到了外层控制器的监听方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{}

从而监听到age的改变

备注:如果添加了kvo后,当前实例对象的isa就一直指向新的子类,如果当前实例对象还有其他属性,不会重写其他属性的set方法。如果设置其他属性等操作,会从子类开始找不到去父类。

那么现在来验证下上面的伪代码是否真的是这样的

先来验证方法实现不一样(methodForSelector就是你传一个方法给我,我给你找到实现,这个方法返回类型是IMP)
IMP在源码里这样定义的
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); //语法的意思是IMP是一个指向函数的指针,该函数返回id并将id,SEL等作为参数
这样说来,IMP是一个指向函数的指针,这个被指向的函数包括id(“self”指针),调用的SEL(方法名),再加上一些其他参数。

NSLog(@"添加KVO之前 p1 =  %p , p2 = %p",[p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector:@selector(setAge:)]);
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后 p1 =  %p , p2 = %p",[p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector:@selector(setAge:)]);
可以看到日志栏输出
2019-11-21 13:36:55.645116+0800 Test[4792:134131] 添加KVO之前 p1 =  0x105a1c000 , p2 = 0x105a1c000
2019-11-21 13:36:55.645338+0800 Test[4792:134131] 添加KVO之后 p1 =  0x7fff25701c8a , p2 = 0x105a1c000

然后我们在xcode日志栏输入打印指令(将IMP指针的地址在控制台转成IMP输出,看看具体值)
(lldb) p (IMP)0x105a1c000
(IMP) $0 = 0x0000000105a1c000 (Test`-[MJPerson setAge:] at MJPerson.h:14)
(lldb) p (IMP)0x7fff25701c8a
(IMP) $1 = 0x00007fff25701c8a (Foundation`_NSSetIntValueAndNotify)//(_NSSetIntValueAndNotify中的Int是因为我们定义的age是int类型的)
可以看出添加kvo后确实改变了调用

监听属性是int时,方法为_NSSetIntValueAndNotify
监听属性是double时,方法为_NSSetDoubleValueAndNotify
监听属性是NSString时,方法为_NSSetObjectValueAndNotify
所以_NSSet*ValueAndNotify这个函数就是添加kvo后调用的

NSKVONotifying_MJPerson类对象里的类结构其他东西是干嘛的呢

//首先写一个运行时,用来打印一个类的对象方法
- (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);//C语言里,methods通过copy出来的,所以要手动释放
}

//创建两个MJPerson对象
MJPerson *p1 = [[MJPerson alloc] init];
p1.age = 1;

MJPerson *p2 = [[MJPerson alloc] init];
p2.age = 2;

// self监听p1的age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];

//调用打印类的对象方法
[self printMethods:object_getClass(p2)];
[self printMethods:object_getClass(p1)];

运行程序,可以看到打印
日志输出:  MJPerson - setAge: age
日志输出:  NSKVONotifying_MJPerson - setAge: class dealloc _isKVOA

由此可以看出NSKVONotifying_MJPerson类对象里的类结构和MJPerson类结构

NSKVONotifying_MJPerson类对象里的类结构

isa     //指向元类对象
superclass   //指向MJPerson类对象
setAge:      //_NSSet*ValueAndNotify
class       // 重写了NSKVONotifying_MJPerson类的实现方法,为了不把这个类暴露出去,效果是return class_getSuperclass(object_getClass(self));不然如果不重写,我们调用[p1 class]时,一步步找到了NSObject,大致效果是NSObject里return object_getClass(self)最后把NSKVONotifying_MJPerson类给返回了出去,这不是苹果想看到的。上文中的p1,p2,我们打印[p1 class]和[p2 class]时,返回的都是MJPerson,这样外界就不知道NSKVONotifying_MJPerson存在。不过,如果我们打印object_getClass(p1)这句话,等同于我们打印了p1->isa,输出的是NSKVONotifying_MJPerson类
dealloc     //  大致是释放kvo吧
isKVOA      //不知道
......

由 如何手动触发KVO?这题展开了论证

题目意思大致就是 如何在不修改值的前提下触发KVO监听方法
答案
手动调用willChangeValueForKey:和didChangeValueForKey:

比如
// 首先要添加了KVO,然后想要手动触发KVO,就写下面两句,必须两句一起用
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
// 然后想要手动触发KVO,就写下面两句,必须两句一起用
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"]; //真正触发监听方法

一直在 说didChangeValueForKey才是触发监听,以及 kvo内部实现是

[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
那么怎么论证呢

//首先在MJPerson.m文件里下实现setAge和willChangeValueForKey和didChangeValueForKey

#import "MJPerson.h"

@implementation MJPerson

- (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");
}

@end

//然后在ViewController控制器里实现下列
- (void)viewDidLoad {
    [super viewDidLoad];
    
    MJPerson *p1 = [[MJPerson alloc] init];
    p1.age = 1;
    
    MJPerson *p2 = [[MJPerson alloc] init];
    p2.age = 2;
    
    // self监听p1的age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    p1.age = 20;
    [p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@改变了 - %@", object, keyPath, change);
}

//可以看到日志台输出
2018-12-26 11:46:08.234305+0800 Interview05-KVO[2750:106490] setAge:
2018-12-26 11:46:08.234468+0800 Interview05-KVO[2750:106490] setAge:
2018-12-26 11:46:08.234917+0800 Interview05-KVO[2750:106490] willChangeValueForKey: - begin
2018-12-26 11:46:08.235120+0800 Interview05-KVO[2750:106490] willChangeValueForKey: - end
2018-12-26 11:46:08.235237+0800 Interview05-KVO[2750:106490] setAge:
2018-12-26 11:46:08.235343+0800 Interview05-KVO[2750:106490] didChangeValueForKey: - begin
2018-12-26 11:46:08.235616+0800 Interview05-KVO[2750:106490] 监听到<MJPerson: 0x60400001ee40>的age改变了 - {
    kind = 1;
    new = 20;
    old = 1;
}
2018-12-26 11:46:08.235762+0800 Interview05-KVO[2750:106490] didChangeValueForKey: - end

由 直接修改成员变量会触发KVO吗?这题展开了论证

不会触发。因为KVO本质是修改set方法,只有调用set才会触发。

//MJPerson类的定义一个成员变量
@interface MJPerson : NSObject
{
@public
    int _age;
}
@end

//然后在ViewController控制器里实现下列
@interface ViewController ()
@property (nonatomic ,strong) MJPerson *pp;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.pp = [[MJPerson alloc] init];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.pp addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.pp->_age = 2;
}

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

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

成员变量是什么

https://www.cnblogs.com/yumian/p/5123596.html
成员变量是定义在{ }号中的变量,如果变量的数据类型是一个类则称这个变量为实例变量。
成员变量不会自动生成set和get方法,需要自己手动实现。
访问成员变量可以通过self->成员变量名字,也可以直接通过变量名字访问。

虽然直接修改成员变量不会触发KVO

#import <Foundation/Foundation.h>
@interface MJPerson : NSObject{
    @public
    int _age;
}
@end
#import "MJPerson.h"

@implementation MJPerson
@end
控制器里
- (void)viewDidLoad {
    [super viewDidLoad];
    MJPerson *p1 = [[MJPerson alloc]init];
    p1->_age = 1;
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    p1->_age = 2; //不会触发KVO
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"---%@",change);
}

但是手动实现成员变量set方法,通过set赋值还是能触发kvo的。但是就算实现了set和get,如果直接调用成员变量(对象->成员变量),还是不能触发。

#import <Foundation/Foundation.h>
@interface MJPerson : NSObject{
    @public
    int _age;
}
-(void)setAge:(int)a;
- (int)age;
@end

#import "MJPerson.h"
@implementation MJPerson
-(void)setAge:(int)a{
    _age = a;
}
- (int)age{
    return _age;
}
@end
控制器里
- (void)viewDidLoad {
    [super viewDidLoad];
    MJPerson *p1 = [[MJPerson alloc]init];
    [p1 setAge:1];
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
    [p1 setAge:2]; //触发KVO
    p1.age = 3;    //触发KVO ,这个p.age跟get方法命名的age没有任何关系,拿的是成员变量的age去走set方法
    p1->_age = 4;  //不会触发KVO
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"---%@",change);
}
上一篇 下一篇

猜你喜欢

热点阅读