将来跳槽用

KVO探究

2018-07-02  本文已影响14人  Maj_sunshine

KVO原理

弄清为什么改变isa指针的指向,能修改调用的方法。

typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

我们可以看出

  1. Class是个objc_class类型的结构体
  2. 所有实例对象的本质是objc_object类型的结构体,里面存放这个实例对象的isa指针。
  3. id是一个objc_object类型的指针,这应该就是id可以指向任意对象的原因
  4. SEL是个objc_selector结构体类型的指针,存放的是方法名
  5. IMP函数指针,存放方法的具体实现地址。
struct objc_class {
    Class _Nonnull isa  // 所属类的指针

#if !__OBJC2__
    Class _Nullable super_class       // 父类
    const char * _Nonnull name      // 类名                         
    long version       // 版本
    long info          // 运行期使用的位标识
    long instance_size            // 实例大小
    struct objc_ivar_list * _Nullable ivars     // 实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists    // 方法列表              
    struct objc_cache * _Nonnull cache      // 方法缓存                   
    struct objc_protocol_list * _Nullable protocols     // 协议列表
#endif

} OBJC2_UNAVAILABLE;
我们可以看出类中也有isa指针,这个isa指针是指向meta元类中。实例对象,类和元类的关系其实有一张图很明显。 0_1326963670oeC1.gif.png

isa的指向是从实例对象->类对象->元类对象->根元类对象->自己。

那我们可以知道,由isa指针指向的对象才是真正调用方法的对象。在类中存储着实例对象的实例方法,元类中存储着类对象的类方法,KVO中的isa- swizzle就是交换isa的指向,本来是在A类中查找调用setter方法,运行时改成了在B类中查找调用setter方法。

使用KVO的方法。

_dog = [[Animal alloc] init];
[_dog setValue:@"牧尘" forKey:@"name"];
// 添加KVO
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

// 触发KVO
_dog.name = @"小明";
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}
- (void)dealloc {
    [_dog removeObserver:self forKeyPath:@"name"];
}
2018-06-29 13:51:37.175922+0800 KVO与NSKeyValueObserving[38410:509677] 新值为小明
2018-06-29 13:51:37.176043+0800 KVO与NSKeyValueObserving[38410:509677] 旧值为牧尘

如果是简单使用,那知道这个就可以了。

使用探究

[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

能用第一种setValue:forKey:方式访问的肯定也能通过setValue: forKeyPath:方式访问,但是通过keyPath方式访问变量不一定能用第一种方式访问。

疑问一 :能通过KVO监听对象中另一个对象的属性么?

新增Food类,实例化为Animal类的对象。

#import <Foundation/Foundation.h>

@class Food;
/**
 动物类
 */
@interface Animal : NSObject
 // 姓名
@property (nonatomic, strong) NSString *name;
 // 食物 
@property (nonatomic, strong) Food *food;

@end


#pragma mark --- 食物
 // 食物类
@interface Food : NSObject
 // 水果
@property (nonatomic, strong) NSString *fruit;

@end
 [_dog setValue:@"苹果" forKeyPath:@"food.fruit"];
[_dog addObserver:self forKeyPath:@"food.fruit" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog setValue:@"荔枝" forKeyPath:@"food.fruit"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"food.fruit"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}
2018-06-29 14:13:45.002673+0800 KVO与NSKeyValueObserving[38503:523330] 新值为荔枝
2018-06-29 14:13:45.002896+0800 KVO与NSKeyValueObserving[38503:523330] 旧值为苹果

可以看出可以通过keyPath的方式对对象的对象进行监听。

疑问二 : 通过keyPath方式监听,只局限于属性监听么,实例对象不能监听么?
@interface Animal() {
    NSString *isName;
}
@end

@implementation Animal

@end
[_dog setValue:@"牧尘" forKey:@"name"];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog setValue:@"小飞" forKey:@"name"];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}
2018-06-29 14:36:51.210152+0800 KVO与NSKeyValueObserving[38565:537594] 新值为小飞
2018-06-29 14:36:51.210349+0800 KVO与NSKeyValueObserving[38565:537594] 旧值为牧尘

可以看出,KVO监听不仅能通过对属性进行监听,还能对类中的实例变量进行监听。

手动触发KVO

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

这个方法的含义是否自动对key进行观察,就是自动触发KVO。默认为YES。我们可以重写方法设置为NO,让我们手动触发。

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

方法来实现对key的手动监听。willChangeValueForKey:顾名思义,将要改变的时候调用的方法,那么didChangeValueForKey:就是已经改变的时候调用的方法,当NSKeyValueObservingOptionNSKeyValueObservingOptionOld| NSKeyValueObservingOptionNew

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _dog = [[Animal alloc] init];
    [_dog setValue:@"牧尘" forKey:@"name"];
    [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    NSLog(@"即将改变name");
    [_dog willChangeValueForKey:@"name"];
//  改变值
    [_dog setValue:@"小飞" forKey:@"name"];
//    _dog.name = @"小明";
    NSLog(@"已经改变name");
    [_dog didChangeValueForKey:@"name"];    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"调用了observeValueForKeyPath方法");
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
        NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
    }
}

打印

2018-06-29 16:58:38.552276+0800 KVO与NSKeyValueObserving[43395:635717] 即将改变name
2018-06-29 16:58:38.552577+0800 KVO与NSKeyValueObserving[43395:635717] 已经改变name
2018-06-29 16:58:38.552752+0800 KVO与NSKeyValueObserving[43395:635717] 调用了observeValueForKeyPath方法
2018-06-29 16:58:38.552873+0800 KVO与NSKeyValueObserving[43395:635717] 新值为小飞
2018-06-29 16:58:38.552998+0800 KVO与NSKeyValueObserving[43395:635717] 旧值为牧尘
当option 为NSKeyValueObservingOptionPrior时
2018-06-29 17:04:28.115616+0800 KVO与NSKeyValueObserving[43412:639884] 即将改变name
2018-06-29 17:04:28.115937+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
2018-06-29 17:04:28.116069+0800 KVO与NSKeyValueObserving[43412:639884] 新值为(null)
2018-06-29 17:04:28.116329+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
2018-06-29 17:04:28.116471+0800 KVO与NSKeyValueObserving[43412:639884] 已经改变name
2018-06-29 17:04:28.116589+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
2018-06-29 17:04:28.116711+0800 KVO与NSKeyValueObserving[43412:639884] 新值为小飞
2018-06-29 17:04:28.116816+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘

runtime到底是什么时候修改了对象的isa指针。

- (NSString *)description {
     // 查看当前isa指针指向的类
    Class runtimeClass = object_getClass(self);
     // 当前类的父类
    Class superClass = class_getSuperclass(runtimeClass);
    NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
    
    return  @"";
}
[_dog description];
[_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_dog description];
2018-06-29 18:09:05.005066+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = Animal, superClass = NSObject
2018-06-29 18:09:05.005591+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = NSKVONotifying_Animal, superClass = Animal

我们可以看出,KVO在- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法调用后,在运行时动态生成了一个原来类的子类NSKVONotifying_XXX,导致我们通过点语法或者 KVC为属性或者实例变量赋值的时候,调用的是这个isa指向的类NSKVONotifying_XXX里的方法列表中的setter方法。

回到上面一个问题,实例变量为什么也能监听?

对实例变量通过KVC访问赋值,由于isa指针的混淆,KVC实际是对NSKVONotifying_XXX类调用。但是由于实例变量无法继承到NSKVONotifying_XXX中,但是KVO仍然会对NSKVONotifying_XXX调用- (void)setValue:(id)value forKey:(NSString *)key方法,NSKVONotifying_XXX调用失败,会调用super类的- (void)setValue:(id)value forKey:(NSString *)key,即赋值成功。我们可以测试一下

- (void)setValue:(id)value forKey:(NSString *)key {
    [super setValue:value forKey:key];
    NSLog(@"animal类中的value = %@", value);
}
- (void)setValue:(id)value forKey:(NSString *)key {
    NSLog(@"cat类中的value = %@",value);
  //  [super setValue:value forKey:key];
}
 Cat *cat = [[Cat alloc] init];
[cat setValue:@"小妖精" forKey:@"name"];
NSLog(@"catName = %@",[cat valueForKey:@"name"]);
NSKeyValueObserving[44918:845002] cat类中的value = 小妖精
2018-07-02 10:10:49.221572+0800 KVO与NSKeyValueObserving[44918:845002] catName = (null)
- (void)setValue:(id)value forKey:(NSString *)key {
    NSLog(@"cat类中的value = %@",value);
    [super setValue:value forKey:key];
}

打印结果,赋值成功,调用了父类中的- (void)setValue:(id)value forKey:(NSString *)key进行赋值,父类中有name实例变量。

NSKeyValueObserving[44937:846921] cat类中的value = 小妖精
2018-07-02 10:13:04.639062+0800 KVO与NSKeyValueObserving[44937:846921] animal类中的value = 小妖精
2018-07-02 10:13:04.639263+0800 KVO与NSKeyValueObserving[44937:846921] catName = 小妖精

KVO分别在KVC赋值的前后调用willChangeValueForKeydidChangeValueForKey实现监听调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context告诉我们值已经改变了。

这样KVC调用的赋值,在我们实际开发中也有用到,类似TextField@"_placeholderLabel.font"这样的键值,我们在子类化TextField,创建subClassTextField这样一个子类后,仍然可以用KVC的方法通过键值对TextFieldplaceholderLabel实例变量进行赋值。

查看KVC对实例变量调用KVO监听过程中,didChangeValueForKey的调用时机。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return YES;
}

重写- (void)setValue:(id)value forKey:(NSString *)key方法

- (void)setValue:(id)value forKey:(NSString *)key {
// 查看当前isa指针指向的类
    Class runtimeClass = object_getClass(self);
    // 当前类的父类
    Class superClass = class_getSuperclass(runtimeClass);
    NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
    NSLog(@"赋值前,name = %@",isName);
    [super setValue:value forKey:key];
    NSLog(@"赋值后,name = %@",isName);
}
2018-07-02 11:31:28.751652+0800 KVO与NSKeyValueObserving[45238:898392] runtimeClass = NSKVONotifying_Animal, superClass = Animal
2018-07-02 11:31:28.752193+0800 KVO与NSKeyValueObserving[45238:898392] 赋值前,name = 牧尘
2018-07-02 11:31:28.752513+0800 KVO与NSKeyValueObserving[45238:898392] 调用了observeValueForKeyPath方法
2018-07-02 11:31:28.752667+0800 KVO与NSKeyValueObserving[45238:898392] 新值为小飞
2018-07-02 11:31:28.752795+0800 KVO与NSKeyValueObserving[45238:898392] 旧值为牧尘
2018-07-02 11:31:28.752940+0800 KVO与NSKeyValueObserving[45238:898392] 赋值后,name = 小飞

我们可以看出,KVC调用实例属性触发didChangeValueForKey是在- (void)setValue:(id)value forKey:(NSString *)key方法中。

我们可以看出KVO监听的过程实际上是对值的监听,在值改变的前后调用willChangeValueForKeydidChangeValueForKey,无论是点语法还是KVC都是直接或间接对值的改变。

手动实现一个KVO

写法思路都一样,推荐一个文章

上一篇下一篇

猜你喜欢

热点阅读