美文网首页
KVO 的实现以及自定义一个kvo

KVO 的实现以及自定义一个kvo

作者: 充满活力的早晨 | 来源:发表于2018-04-26 13:43 被阅读31次

    KVO (Key-Value Observing)

    KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。

    KVO 基本用法

    我们知道key 可以观察对象某个属性发生变化,或者keypath发生的变化,下面是代码实例。

    #import <Foundation/Foundation.h>
    #import "Man.h"
    @interface Person : NSObject
    @property (nonatomic,strong) Man *man;
    @property (nonatomic,strong) NSString *name;
    @end
    
    #import "Person.h"
    
    @implementation Person
    
    @end
    
    #import <Foundation/Foundation.h>
    @interface Man : NSObject
    @property (nonatomic,strong) NSString  *age;
    @end
    
    #import "Man.h"
    @implementation Man
    
    @end
    

    我们定义一个Person 和Man 两个类,Man 是Person中的属性。Person 还有个属性name。man有个属性age
    测试代码

    - (void)viewDidLoad {
        [super viewDidLoad];
        Person * p=[[Person alloc]init];
        
        [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        [p addObserver:self forKeyPath:@"man.age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        p.name = @"KVO";
        Man * man = [[Man alloc]init];
        p.man = man;
        man.age = @"18";
        
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@",keyPath);
    }
    
    

    测试结果是

    2018-04-26 09:21:11.278539+0800 KVO 实现[41309:1427305] name
    2018-04-26 09:21:11.278926+0800 KVO 实现[41309:1427305] man.age
    2018-04-26 09:21:11.279126+0800 KVO 实现[41309:1427305] man.age
    

    原理探索

    KVO用法简单,但是苹果是怎么实现KVO的呢?
    苹果官网有这么句话

    Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ..
    用了isa 交换 。

    当我们调用方法的时候

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    

    苹果把调用者的isa 指向换成了NSKVONotifying_XXX

    image.png
    从上图我们能看出来Person 的指针p 指向了NSKVONotifying_Person类

    那我们看看NSKVONotifying_Person 类到底有什么方法。

    新增一个类

    #import <Foundation/Foundation.h>
    
    @interface ObjcClassMethodParse : NSObject
    - (instancetype)initWithClass:(Class)cls;
    -(void)print;
    
    @end
    
    
    #import "ObjcClassMethodParse.h"
    #import <objc/runtime.h>
    @interface ObjcClassMethod: NSObject
    @property (nonatomic,strong) NSString *name;
    @property (nonatomic,strong) NSString *value;
    
    @end
    
    @implementation ObjcClassMethod
    
    @end
    
    @interface ObjcClassMethodParse()
    @property (nonatomic,strong) Class cls;
    @property (nonatomic,strong) NSMutableArray * methodListArr;
    @end
    @implementation ObjcClassMethodParse
    - (instancetype)initWithClass:(Class)cls
    {
        self = [super init];
        if (self) {
            self.cls = cls;
            self.methodListArr = [NSMutableArray array];
            [self parse];
            
        }
        return self;
    }
    //https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1
    -(void)parse{
        
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(self.cls, &count);
        for (int i=0; i<count; i++) {
            Method method = methodList[i];
            ObjcClassMethod * m=[[ObjcClassMethod alloc]init];
         const char * name= sel_getName(method_getName(method));
            m.name = [NSString stringWithFormat:@"%s",name];
            [self.methodListArr addObject:m];
        }
        free(methodList);
        
    }
    
    -(void)print{
        for (ObjcClassMethod *m in self.methodListArr) {
            NSLog(@"name %@",m.name);
        }
    }
    @end
    

    我们通过这个类看看到底NSKVONotifying_Person 有啥方法
    测试代码

     struct objc_object * obj = (__bridge  struct objc_object *)p;
        Class objClass = obj->isa;
        NSLog(@"%@",objClass);
        ObjcClassMethodParse * methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
        [methodParse print];
    

    测试结果

    2018-04-26 09:42:09.377354+0800 KVO 实现[46687:1454265] NSKVONotifying_Person
    2018-04-26 09:42:09.377683+0800 KVO 实现[46687:1454265] name setMan:
    2018-04-26 09:42:09.377960+0800 KVO 实现[46687:1454265] name setName:
    2018-04-26 09:42:09.378149+0800 KVO 实现[46687:1454265] name class
    2018-04-26 09:42:09.378497+0800 KVO 实现[46687:1454265] name dealloc
    2018-04-26 09:42:09.378720+0800 KVO 实现[46687:1454265] name _isKVOA
    

    NSKVONotifying_Person 类,修改了class方法,dealloc方法,新增_isKVOA 方法,并且重新实现了两个方法,setMan: setName: 。 这里需要注意,我们修改的是man.age。这里修改的是setMan: 方法,这里我们猜想Man类应该也被修改成了NSKVONotifying_Man, NSKVONotifying_Man 有个新增方法setAge:.
    证明我们的猜想
    测试代码

        NSLog(@"man 赋值给p前函数");
        Man * man = [[Man alloc]init];
         obj = (__bridge  struct objc_object *)man;
         objClass = obj->isa;
        NSLog(@"%@",objClass);
       methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
        [methodParse print];
        NSLog(@"man 赋值给后前函数");
        p.man = man;
        obj = (__bridge  struct objc_object *)man;
        objClass = obj->isa;
        NSLog(@"%@",objClass);
        methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
        [methodParse print];
        NSLog(@"====end");
    

    结果显示如下

    2018-04-26 10:03:35.707870+0800 KVO 实现[52216:1485542] man 赋值给p前函数
    2018-04-26 10:03:35.708014+0800 KVO 实现[52216:1485542] Man
    2018-04-26 10:03:35.708140+0800 KVO 实现[52216:1485542] name .cxx_destruct
    2018-04-26 10:03:35.708243+0800 KVO 实现[52216:1485542] name setAge:
    2018-04-26 10:03:35.708338+0800 KVO 实现[52216:1485542] name age
    2018-04-26 10:03:35.708483+0800 KVO 实现[52216:1485542] man 赋值给后前函数
    2018-04-26 10:03:35.709002+0800 KVO 实现[52216:1485542] NSKVONotifying_Man
    2018-04-26 10:03:35.709149+0800 KVO 实现[52216:1485542] name setAge:
    2018-04-26 10:03:35.709258+0800 KVO 实现[52216:1485542] name class
    2018-04-26 10:03:35.709433+0800 KVO 实现[52216:1485542] name dealloc
    2018-04-26 10:03:35.709544+0800 KVO 实现[52216:1485542] name _isKVOA
    2018-04-26 10:03:35.709783+0800 KVO 实现[52216:1485542] ====end
    

    这里我们就看出来,我们的猜想是正确的。只不过修改成NSKVONotifying_Man 类是发生在赋值的时候。

    我知道我们可以正常的给没有增加kvo监听的属性正常赋值,我们看NSKVONotifying_Man 也没有相关方法,这怎么做到的呢?就是继承啦,我继承Man 类,我就有了Man的所有功能了,NSKVONotifying_Man只要我不认识的功能,那么我就直接调用父类就行了。

    这里还有注意一点,我们给man.age keyPath 增加了KVO。Person中,新增的方法是setMan: 传入的参数 找到这个方法找到setAge: 这就有点递归的意思了。

    这里还有个方法_isKVOA,这个方法就是给KVO实现的类的标记,只要是KVO生成的类,都是返回YES。
    测试代码

    -(void)test4{
        Person * p=[[Person alloc]init];
        struct objc_object * obj = (__bridge  struct objc_object *)p;
        Class objClass = obj->isa;
        NSLog(@"%@",objClass);
        ObjcClassMethodParse * methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
        [methodParse print];
        [p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
        [p addObserver:self forKeyPath:@"man.age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        obj = (__bridge  struct objc_object *)p;
        objClass = obj->isa;
        NSLog(@"%@",objClass);
        void * mm = &objClass;
        SEL sel = NSSelectorFromString(@"_isKVOA");
        bool is= objc_msgSend((__bridge id)mm, sel);
        NSLog(@"%d" ,is);
        methodParse =[[ObjcClassMethodParse alloc]initWithClass:objClass];
        [methodParse print];
        
    }
    

    测试结果

    2018-04-26 13:54:50.105262+0800 KVO 实现[11082:1745174] Person
    2018-04-26 13:54:50.105516+0800 KVO 实现[11082:1745174] name setMan:
    2018-04-26 13:54:50.105666+0800 KVO 实现[11082:1745174] name man
    2018-04-26 13:54:50.105963+0800 KVO 实现[11082:1745174] name carId
    2018-04-26 13:54:50.106227+0800 KVO 实现[11082:1745174] name setCarId:
    2018-04-26 13:54:50.106437+0800 KVO 实现[11082:1745174] name name
    2018-04-26 13:54:50.106608+0800 KVO 实现[11082:1745174] name .cxx_destruct
    2018-04-26 13:54:50.106883+0800 KVO 实现[11082:1745174] name setName:
    2018-04-26 13:54:50.107549+0800 KVO 实现[11082:1745174] NSKVONotifying_Person
    2018-04-26 13:54:50.107675+0800 KVO 实现[11082:1745174] 1
    2018-04-26 13:54:50.107814+0800 KVO 实现[11082:1745174] name setMan:
    2018-04-26 13:54:50.107899+0800 KVO 实现[11082:1745174] name setName:
    2018-04-26 13:54:50.108013+0800 KVO 实现[11082:1745174] name class
    2018-04-26 13:54:50.108136+0800 KVO 实现[11082:1745174] name dealloc
    2018-04-26 13:54:50.108287+0800 KVO 实现[11082:1745174] name _isKVOA
    
    

    返回的是1 ,YES。(不好意思,这里没有对每个方法进行参数获取,我想着应该是基础的部分,大家自己通过method_copyArgumentType获取下参数看看吧)

    实现KVO

    新增自定义KVO category

    #import <Foundation/Foundation.h>
    
    @interface NSObject (CustomKVO)
    -(void)CTKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
    @end
    

    import "NSObject+CustomKVO.h"

    import <objc/message.h>

    @implementation NSObject (CustomKVO)
    -(void)CTKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{

    //动态添加一个类 去重
    NSString *oldClassName = NSStringFromClass([self class]);
    Class myclass = NSClassFromString(oldClassName);
    SEL methodSel = NSSelectorFromString(@"_isKVOA");
    if ([oldClassName hasPrefix:@"CTKVO_"]) {
        BOOL is= objc_msgSend(self,methodSel);
        if (!is) {
            NSString *newClassName = [@"CTKVO_" stringByAppendingString:oldClassName];
            const char * newName = [newClassName UTF8String];
            myclass = objc_allocateClassPair([self class], newName, 0);
            class_addMethod(myclass, methodSel, (IMP)_isKVOA, "v@:");
            //注册新添加的这个类
            objc_registerClassPair(myclass);
            ///isa 改变
            object_setClass(self, myclass);
        }
    }else{
        NSString *newClassName = [@"CTKVO_" stringByAppendingString:oldClassName];
        const char * newName = [newClassName UTF8String];
        myclass = objc_allocateClassPair([self class], newName, 0);
        class_addMethod(myclass, methodSel, (IMP)_isKVOA, "v@:");
        //注册新添加的这个类
        objc_registerClassPair(myclass);
        ///isa 改变
        object_setClass(self, myclass);
    }
    
    NSMutableDictionary * keyPathDic= objc_getAssociatedObject(self, (__bridge const void *)@"kvo_keyPath");
    if (!keyPathDic) {
        keyPathDic= [NSMutableDictionary dictionary];
       objc_setAssociatedObject(self, (__bridge const void *)@"kvo_keyPath", keyPathDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    NSMutableArray * keyPathValueArr = nil;
    if (![keyPathDic objectForKey:keyPath]) {
        keyPathValueArr = [NSMutableArray array];
        [keyPathDic setObject:keyPathValueArr forKey:keyPath];
    }
    
    BOOL isHave = NO;
    for (NSDictionary * observerInfo in keyPathValueArr) {
       NSObject * obs = [observerInfo objectForKey:@"kvo_observer"];
        if (obs && obs == observer) {
            isHave = YES;
        }
    }
    if (!isHave) {
        NSMutableDictionary  * observerInfo= [NSMutableDictionary dictionary];
        [observerInfo setObject:observer forKey:@"kvo_observer"];
        [observerInfo setObject:@(options) forKey:@"kvo_options"];
        if (context) {
            [observerInfo setObject:(__bridge id)context forKey:@"kvo_context"];
        }
        [keyPathValueArr addObject:observerInfo];
    }
    

    ///alloc init
    NSArray * array= [keyPath componentsSeparatedByString:@"."];
    NSString * methodName = nil;
    if (array.count>0) {
    methodName = array[0];
    }
    methodName =[methodName capitalizedString];
    methodName =[[@"set" stringByAppendingString:methodName]stringByAppendingString:@":"];
    methodSel = NSSelectorFromString(methodName);
    class_addMethod(myclass, methodSel, (IMP)CustomKVOIMP, "v@:@");

    }

    //相当于重写父类的方法
    void CustomKVOIMP(id self, SEL sel, id param) {

    NSMutableDictionary * keyPathDic= objc_getAssociatedObject(self, (__bridge const void *)@"kvo_keyPath");
    NSString * selName = NSStringFromSelector(sel);
    selName = [selName substringFromIndex:3];
    selName = [selName substringToIndex:selName.length-1];
    selName = [selName lowercaseString];
    NSMutableArray * selPathArr= [NSMutableArray array];
    for (NSString * key in keyPathDic.allKeys) {
       NSString * lKey = [key lowercaseString];
        if ([lKey hasPrefix:selName]) {
            [selPathArr addObject:key];
        }
    }
    
    for (NSString *keyPath in selPathArr) {
        ///所有观察者
        NSArray * array = [keyPathDic objectForKey:keyPath];
        //保存当前类
        Class myclass = [self class];
        //将self的isa指针指向父类
        object_setClass(self, class_getSuperclass([self class]));
        objc_msgSend(self, sel,param);
        
        NSArray * arrayPath= [keyPath componentsSeparatedByString:@"."];
        NSMutableArray * apaths =[NSMutableArray arrayWithArray:arrayPath];
        [apaths removeObjectAtIndex:0];
        NSString * path = [apaths componentsJoinedByString:@"."];
    
        SEL getMethod = NSSelectorFromString(selName);
        
    
        id getOldValue =objc_msgSend(self, getMethod);
        
        ///这里让path 的下面的对象 更改isa
        for (NSDictionary * info in array) {
            id obs = [info objectForKey:@"kvo_observer"];
            NSNumber * op = [info objectForKey:@"kvo_options"];
            void * content = (__bridge void *)([info objectForKey:@"kvo_context"]);
            NSMutableDictionary * dic=[NSMutableDictionary dictionary];
            if (getOldValue) {
                [dic setObject:getOldValue forKey:@"old"];
            }
            [dic setObject:param forKey:@"new"];
    
            objc_msgSend(obs,@selector(observeValueForKeyPath:ofObject:change:context:),keyPath,self,dic,content);
            
            if (path) {
                [param CTKVO_addObserver:obs forKeyPath:path options:op.intValue context:content];
            }
        }
        object_setClass(self, myclass);
    }
    

    }

    BOOL _isKVOA(id self, SEL sel){
    return YES;
    }

    @end

    调用
    

    -(void)test2{
    Person * p=[[Person alloc]init];
    [p CTKVO_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [p CTKVO_addObserver:self forKeyPath:@"carId" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [p CTKVO_addObserver:self forKeyPath:@"man.age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    Man * man = [[Man alloc]init];
    p.man = man;
    man.age = @"18";
    }

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"keyPath=%@ object=%@ change=%@ context=%@",keyPath,object,change,context);
    }

    测试结果
    

    2018-04-26 13:33:18.648556+0800 KVO 实现[5534:1718809] keyPath=man.age object=<Person: 0x600000222300> change={
    new = "<Man: 0x600000010660>";
    old = "<Man: 0x600000010660>";
    } context=(null)
    2018-04-26 13:33:18.648868+0800 KVO 实现[5534:1718809] keyPath=age object=<Man: 0x600000010660> change={
    new = 18;
    old = 18;
    } context=(null)

    
    上面实现自定义KVO 。不是很细,只是让kvo 增加 keyPath支持。
    比如
    在kvo中增加
    > willChangeValueForKey
    > didChangeValueForKey
    没有关心,这不影响KVO实现的基本流程。
    
    实现流程
    
    > 1.自定义一个类,继承 [self class]的一个子类
    > 2.重写父类的需要观察的属性setter方法
    > 3.在实现的setting 方法中解析path 路径,进行递归增加路径上的类
    > 4.在子类的setting方法中,让observer 调用observeValueForKeyPath:ofObject:change:context:方法。
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    相关文章

      网友评论

          本文标题:KVO 的实现以及自定义一个kvo

          本文链接:https://www.haomeiwen.com/subject/jhyylftx.html