由浅入深研究KVO

KVO 是 Objective-C 对观察者模式(Observe Pattern)的实现,也是Cocoa Binding 的基础。当某个属性发生更改时,观察者对象会获得通知。使用方便,你不需要给被观察者对象添加任何额外的代码就能实现。这是怎么做到的呢?

一、KVO的实现原理

新建一个 Person_KVO 类,继承自NSObject,并添加一个属性 name,做如下处理:

Person_KVO.h
@interface Person_KVO : NSObject
@property(nonatomic,strong)NSString * name;
@end
*****************************************************************************
KVOController.m
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person_KVO *p = [[Person_KVO alloc]init];// A
    [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];// B
     _p = p;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    static int a = 0;
    a++;
    self.p.name = [NSString stringWithFormat:@"Tom-%d",a];
}
@end

此时,如果我们点击屏幕就会得如下输出结果:
image.png

接下来我们在 viewDidLoad 方法中打个断点,重新运行一下程序,当执行完 A 行代码时看下对象 p 的情况:
由浅入深研究KVO_第1张图片
image.png
当执行完 B 行代码后,再看下 p 的情况:
由浅入深研究KVO_第2张图片
image.png
可以发现,p 对象在添加观察者之后类型由原来的 Person_KVO 变成了 NSKVONotifying_Person_KVO 类型。什么原因呢?
一个对象在调用 addObserver 方法时,会动态创建一个带有NSKVONotifying_ 前缀的原类的子类,用来重写所有的 setter 方法,并且子类的 - (Class)class 和 -(Class)superClass 方法会被重写,返回子类(带前缀的类)的类型和父类(原始类)的类型,最后将对象的类型改为这个带前缀的子类的类型。

因此,当我们调用 p 的 setName: 方法时,实际上调用的是在 NSKVONotifying_Person_KVO 中重写的:

@implementation NSKVONotyfing_Person_KVO
- (void)setName:(NSString *)name
{
    [self willChangeValueForKey:@"name"];
    [super setName:name];//设置新的属性值
    [self didChangeValueForKey:@"name"];//通知观察者值的变化
}

二、利用 Runtime 实现上述案例

新建一个关于 NSObejct 的类别:

#import 
@interface NSObject (LFKVO)
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

#import "NSObject+LFKVO.h"
#import 
@implementation NSObject (LFKVO)
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    /*
     1.动态创建一个类
     2.改变方法调用者的类型!!self
     */
    
    //1.动态创建一个类.(1)继承谁 (2)类名字
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [@"LFKVO_" stringByAppendingString:oldName];
    Class NewClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    //注册该类
    objc_registerClassPair(NewClass);
    
    //2.改变类的类型
    object_setClass(self, NewClass);
    
    //3.添加setName方法(子类里没有该方法),重写父类方法就是添加一个一模一样的方法
    class_addMethod(NewClass, @selector(setName:), (IMP)setName,"V@:@");
    
    //4.将观察者绑定为属性
    objc_setAssociatedObject(self, @"LF", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self,SEL _cmd,id newName)
{
    NSLog(@"新来的 name 是:%@",newName);
    id class = [self class];
    //调用一下父类的setName方法
    object_setClass(self, class_getSuperclass([self class]));//改变类型为父类类型
    objc_msgSend(self, _cmd,newName);
    //通知一下外界
    id observer = objc_getAssociatedObject(self, @"LF");
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new":newName},nil);
    //改回原类型
    object_setClass(self, class);
}
@end
*****************************************************************
#import "KVOController.h"
#import "Person_KVO.h"
#import "NSObject+LFKVO.h"
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person_KVO *p = [[Person_KVO alloc]init];
        [p LF_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
     _p = p;
}

运行后点击屏幕,输出结果如下
由浅入深研究KVO_第3张图片
image.png

三、优化上述案例

上面的写法是为了帮助大家理解 KVO 底层的实现方式,但如果 Person_KVO 类新增加了一个属性 hobby,能否也能用于监听该 hobby属性呢?

#import 
@interface Person_KVO : NSObject
@property(nonatomic,strong)NSString * name;
@property(nonatomic,strong)NSString * hobby;
@end

******************************************************************

#import "KVOController.h"
#import "Person_KVO.h"
#import "NSObject+LFKVO.h"
@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person_KVO *p = [[Person_KVO alloc]init];
        [p LF_addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew context:nil];
   
    _p = p;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    static int b = 10;
    b++;
    self.p.hobby = [NSString stringWithFormat:@"dance-%003d",b];
}

运行后,点击屏幕输出结果如下:
image.png

没有任何输出。仔细看下上面 Runtime 的实现,细心的小伙伴会发现这个只是针对 name 属性的监听实现。想要监听所有的属性,需要按如下方案优化:

#import "NSObject+LF_KVO.h"
#import 

NSString *const kLFKVOClassPrefix = @"LFKVO_";
NSString *const kLFKVOObserverKey = @"LFKVOObserverKey";
@implementation NSObject (LF_KVO)
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    //1.动态创建一个类 (1)继承谁 (2)类名字
    NSString * oldClassName = NSStringFromClass([self class]);
    NSString * newClassName = [[NSString alloc]initWithString:oldClassName];
    if(![oldClassName hasPrefix:kLFKVOClassPrefix])
    {
        newClassName = [kLFKVOClassPrefix stringByAppendingString:oldClassName];
        Class NewClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        //注册该类
        objc_registerClassPair(NewClass);
        //2.改变类的类型
        object_setClass(self, NewClass);
    }
    //3.添加 set 方法(子类在底层没有该方法),重写父类方法就是添加一个一模一样的方法
    SEL setSelector = [self setSelectorFromString:keyPath];
    Method setterMethod = class_getInstanceMethod([self superclass], setSelector);
    if(!setterMethod)
    {
        NSString *reason =[NSString stringWithFormat:@"%@ 类没有关于 %@ 的 set 方法",NSStringFromClass([self superclass]),keyPath];
        NSLog(@"%@",reason);
        return;
    }
    class_addMethod([self class], setSelector, (IMP)kvo_setter, method_getTypeEncoding(setterMethod));
    
    //4.绑定观察者为属性
    objc_setAssociatedObject(self, (__bridge const void*)kLFKVOObserverKey, observer, OBJC_ASSOCIATION_ASSIGN);
    
}
     
static void kvo_setter(id self,SEL _cmd,id newValue)
{
    NSString * key = [self getterNameFromSetSelector:_cmd];
    NSLog(@"新来的 key 是:%@",key);
    NSLog(@"新来的 value 是:%@",newValue);
    
    Class class = object_getClass(self);
    object_setClass(self,class_getSuperclass([self class]));//改变类型为父类类型
    //改变属性值
    objc_msgSend(self, _cmd,newValue);
    //通知外界
    id observer = objc_getAssociatedObject(self, (__bridge const void*)kLFKVOObserverKey);
    objc_msgSend(observer,
                 @selector(observeValueForKeyPath:ofObject:change:context:),
                 key,self,@{@"new":newValue},nil);

    object_setClass(self,class);//改回原类型
}

#pragma mark--根据key值获取对应的set方法
- (SEL)setSelectorFromString:(NSString *)key{
    if(!key.length)
    {
        return nil;
    }
    NSString * prefixStr = [[key substringToIndex:1] uppercaseString];
    NSString * secondStr = [key substringFromIndex:1];
    
    return NSSelectorFromString([NSString stringWithFormat:@"set%@%@:",prefixStr,secondStr]);
}
#pragma mark-- 根据set方法获取对应的key值
- (NSString *)getterNameFromSetSelector:(SEL)selector
{
    NSString * setterName = NSStringFromSelector(selector);
    if(!setterName.length)
    {
        return nil;
    }
    NSString * getterPrefix = [[setterName substringWithRange:NSMakeRange(3, 1)]lowercaseString];
    NSString *getterSecond = [setterName substringWithRange:NSMakeRange(4, setterName.length - 5)];
    return [NSString stringWithFormat:@"%@%@",getterPrefix,getterSecond];
}
@end
********************************************************

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person_KVO * p = [[Person_KVO alloc]init];
    
    [p LF_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    [p LF_addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew context:nil];
    
    _p = p;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);

}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    static int a = 0;
    a++;
    _p.name = [NSString stringWithFormat:@"Tome_%03d",a];
    
    static int b = 10;
    b++;
    _p.hobby = [NSString stringWithFormat:@"thong_%03d",b];
}

运行后,点击屏幕结果如下:
由浅入深研究KVO_第4张图片
image.png

四、优化系统KVO的使用方式

当我们所监听对象的属性改变后,会做如下处理

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{ 
    if([keyPath isEqualToString:@"name"])
    {
        //处理
    }
    if([keyPath isEqualToString:@"hobby"])
    {
        //处理
    }
    ......
}

如果监听的属性比较多的话,就需要在observeValueForKeyPath:方法中做多种情况处理,使用起来很不友好。我想到了如下方案:

#import 

typedef void (^KVO_ObserverBlock)(NSString *keyPath,id object,NSDictionary *change);
@interface NSObject (LFKVO)
@property (nonatomic,strong)KVO_ObserverBlock kvo_observerBlock;
- (void)WLF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context block:(KVO_ObserverBlock)block;
@end

#import "NSObject+WLFKVO.h"
#import 
@implementation NSObject (LFKVO)

static char kvo_key;
- (void)setKvo_observerBlock:(KVO_ObserverBlock)kvo_observerBlock
{
    objc_setAssociatedObject(self, &kvo_key, kvo_observerBlock, OBJC_ASSOCIATION_COPY);
}
- (KVO_ObserverBlock)kvo_observerBlock
{
    return objc_getAssociatedObject(self, &kvo_key);
}
- (void)LF_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context block:(KVO_ObserverBlock)block
{
    self.kvo_observerBlock = block;
    [self addObserver:observer forKeyPath:keyPath options:options context:context];
}

使用方式为:

@interface KVOController ()
@property(nonatomic,strong) Person_KVO *p;
@end
@implementation KVOController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person_KVO *p = [[Person_KVO alloc]init];
    
     [p LF_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil block:^(NSString *keyPath, id object, NSDictionary *change) {
     
         NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
     }];
    
     //
     [p LF_addObserver:self forKeyPath:@"hobby" options:NSKeyValueObservingOptionNew context:nil block:^(NSString *keyPath, id object, NSDictionary *change) {
     
         NSLog(@"观察到了 %@ 的 %@ 属性变化为了:%@",NSStringFromClass([object class]),keyPath,change[@"new"]);
     }];
     

    _p = p;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    _p.kvo_observerBlock(keyPath, object, change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    static int a = 0;
    a++;
    self.p.name = [NSString stringWithFormat:@"Tom-%d",a];
    
    static int b = 10;
    b++;
    self.p.hobby = [NSString stringWithFormat:@"dance-%003d",b];
}

大家谁有更好的实现方式可以随时告诉我啊

五、总结

看到了几个较好的问题,以此来做个总结吧。

keyPath是什么

首先keyPath,是对于setter方法的关联,会使用keypath作为后缀去寻找原类的setter方法的方法签名,和实际存取对象和property名称没有关系。所以这也是为什么我们重命名了setter方法之后,没有办法再去使用KVO或KVC了,需要手动调用一次willChangeValue方法。

子类继承父类的一个属性,当这个属性被改变时,KVO能否观察到?

因为继承的关系Father <- Son <- KVOSon,当我监听一个父类属性的keyPath的时候,Son实例同样可以通过消息查找找到父类的setter方法,再将该方法加入到KVOSon类当中去。

子类继承父类的一个未暴露的属性,当这个属性被改变时,KVO能否观察到?

由于在overrideSetterFor中,我们是直接通过sel去得到方法签名signature,所以和暴不暴露没啥关系。

子类继承父类属性并重写了它的setter方法,当这个属性被改变时,KVO能否观察到?

在上一条中知道,其实子类监听父类属性,并不依赖继承,而是通过ISA指针在消息转发的时候能够获取到父类方法就足够。所以当我们重写父类setter方法,相当于在子类定义了该setter函数,在我们去用sel找方法签名时,直接在子类中就拿到了,甚至都不需要去到父类里。所以理解了KVO监听父类属性和继承没有直接联系这一点,就不再纠结set方法是否重写这个问题了。

欢迎大家多提宝贵意见~~

你可能感兴趣的:(由浅入深研究KVO)