KVO实现原理

概览

本文分为两个大的方面。一、kvo的简单使用场景。二、kvo的来龙去脉,讲讲苹果的实现。

KVO 使用方法,和常用场景。

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

— Key-Value Observing Programming Guide

简而言之,kvo就是允许一个对象去监听其他对象(可以自己)指定属性的值的变化。但是一般涉及的类比较复杂的时候,我们应该该使用Notification或者delegate,b不然太过分散,bug不容易查找,当然delegate,通知也需要统一处理.现在使用属性监听的场景还是比较少了,我们这里主要是探究一下苹果的实现原理。

使用方法分3步:

1.  注册观察者 addObserver:forKeyPath:options:context:
2.  观察者中实现
    observeValueForKeyPath:ofObject:change:context:
3.  移除观察者: removeObserver:forKeyPath:

注意:

  • 注册与移除必须成对出现,否则会crash掉。
  • 观察者实现的方法,change字典里存放的数据与你注册观察者时 的options相关,NSKeyValueObservingOptionNew表现为 改变后的值,键为@”new”;NSKeyValueObservingOptionOld, 同理键为@”old”,根据自己的需要选择。

kvo实现原理

1.  runtime生成被监控类的子类NSKVONotifying_xx实例对
    象,被监控对象的isa指针指向子类,真正的起作用的类就成了
    子类。

2. 一旦被监控类的某个属性改变,就会在子类中重写相应的set方
   法,在set方法中调用NSObject的- willChangeValueForKey:
   和- didChangeValueForKey:通知观察者。自己可以测试在被
   监控的类中自己重写这两个方法中的一个,可以看到观察者就
   收不到
   -observeValueForKeyPath:ofObject:change:context:消息
   了,说明截断了消息,使得kvo机制不起作用了。

3. 子类中还重写了- class方法,返回父类的 class,欲盖弥彰,
   就好像没有这个子类一样。

4.删除观察者后一切照旧,对象的isa指针重新指向父类。

下面通过代码来验证:

自定义Person类,有age和height两个属性。自己时被监控对象,为了简单起见,也是观察者。

#import 
#import 

@interface Person : NSObject

@property(nonatomic,assign) int age;
@property(nonatomic,assign) float height;

@end

@implementation Person
/**
 *  如果重写,这两个方法,kvo就失效了。
 */
//- (void)willChangeValueForKey:(NSString *)key{
//    NSLog(@"willChangeValueForKey");
//}

//- (void)didChangeValueForKey:(NSString *)key{
//    NSLog(@"didChangeValueForKey");
//}
//options属性改变change的值,这个是观察者要实现的方法。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"%@",change);
    }

}
@end

获取一个类实现的所有selector(不包括父类的方法)

static NSArray* classMethodsName(Class c){
    NSMutableArray* array = [NSMutableArray new];
    uint count = 0;
    Method* methods = class_copyMethodList(c, &count);
    for(int i=0; iNSStringFromSelector(
                      method_getName(methods[i])
                      )
        ];
    }
    return array;
}

static void PrintDescription(NSString *name, id obj)
{
    //重点关注,对象的类型,runtime的类型。
    NSString *str = [NSString stringWithFormat:
                     @"%@: %@\n\tNSObject class %@\n\tlibobjc class %@\n\timplements methods ",
                     name,
                     obj,
                     [obj class],
                     object_getClass(obj),
                     [                          
                     classMethodsName(
                     object_getClass(obj)
                     ) 
                     componentsJoinedByString:@","]
                     ];

    printf("%s\n", [str UTF8String]); 
}

main函数里,定义了三个人,one,two,three,one观察了自己的age属性,two观察了自己height属性,three作为对比。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person* one = [Person new];
        Person* two = [Person new];
        Person* three = [Person new];

        printf("注册观察者之前\n");
        PrintDescription(@"one", one);
        PrintDescription(@"two", two);
        PrintDescription(@"three", three);
        /**
         *  在注册观察者之前
         */
       //breakpint 1
        [one addObserver:one forKeyPath:@"age" 
             options:NSKeyValueObservingOptionNew 
             context:nil];
        [two addObserver:two forKeyPath:@"height" 
             options:NSKeyValueObservingOptionNew 
             context:nil];

        printf("注册观察者之后\n");        
        PrintDescription(@"one", one);
        PrintDescription(@"two", two);
        PrintDescription(@"three", three);

        //查看类的方法实现(函数指针)的地址。
        printf("\none own method setAge:%p  libSubcluss 
               method setAge:%p\n",
               class_getMethodImplementation(
                  [one class], @selector(setAge:)
               ),
               class_getMethodImplementation(
                 object_getClass(one), 
                 @selector(setAge:)
                 )
        );
        printf("two own method setHeight:%p
                libSubcluss method setHeight:%p\n"
               ,class_getMethodImplementation(
                  [two class], @selector(setHeight:)
                ),
               class_getMethodImplementation(
                 object_getClass(two), 
                 @selector(setHeight:)
               )
        );
        printf("three own method setHeight:%p  
            three libSubcluss method setHeight:%p\n\n",
            class_getMethodImplementation(
              [three class], @selector(setHeight:)
            ),
            class_getMethodImplementation(
              object_getClass(three), 
              @selector(setHeight:)
              )
         );
         //        one.age = 14;
        //        two.height = 5.5;
        /**
         *  注册观察者之后
         */      
       //breakpoint 2
        [one removeObserver:one forKeyPath:@"age"];
        [two removeObserver:two forKeyPath:@"height"];

        printf("删除观察者之后\n");
        PrintDescription(@"one", one);
        PrintDescription(@"two", two);
        PrintDescription(@"three", three);
        //breakpoint 3

    }
    return 0;
}

breakpint 1

one: <Person: 0x1006001c0>
    NSObject class Person
    libobjc class Person
    implements methods   
    setAge:,observeValueForKeyPath:ofObject:change
        :context:,height,setHeight:>
two: <Person: 0x100600220>
    NSObject class Person
    libobjc class Person
    implements methods 
    setAge:,observeValueForKeyPath:ofObject:change
        :context:,height,setHeight:>
three: <Person: 0x100600230>
    NSObject class Person
    libobjc class Person
    implements methods 
    setAge:,observeValueForKeyPath:ofObject:change
        :context:,height,setHeight:>

结果1:

在未添加观察者之前,运行时的类和对象本身的类是一样的。

breakpoint2

one: 0x1006001c0>
    NSObject class Person
    libobjc class NSKVONotifying_Person
    implements methods 
    class,dealloc,_isKVOA>
two: 0x100600220>
    NSObject class Person
    libobjc class NSKVONotifying_Person
    implements methods 
    class,dealloc,_isKVOA>
three: 0x100600230>
    NSObject class Person
    libobjc class Person
    implements methods 
    

one own method setAge:0x1000015b0  libSubcluss method     
               setAge:0x7fff8a5a1a81
two own method setHeight:0x100001600  libSubcluss 
        method setHeight:0x7fff8a5a1ba9
three own method   setHeight:0x100001600  three  
libSubcluss method setHeight:0x100001600

结果2

1.  监听过的属性值都会在NSKVONotifying_XX(本例是Person)
    生成对应的set方法。
2. 重写了class方法,目的在于隐藏子类,依然返回父类的class,
    伪装自己。
3.  one的setAge方法,two的setHeight方法,居然有两个实
     现,说明运行时至少是该方法重写了。而没有监听属性的
     three一切正常。
至此,应该算是比较明白runtime干了一件什么样的事了,还不会,那我们看看,删除监听的后效果。

breakpoint3

one: <Person: 0x1006001c0>
    NSObject class Person
    libobjc class Person
    implements methods
    setAge:,observeValueForKeyPath:ofObject:change
    :context:,height,setHeight:>
two: <Person: 0x100600220>
    NSObject class Person
    libobjc class Person
    implements methods 
   setAge:,observeValueForKeyPath:ofObject:change:
      context:,height,setHeight:>
three: <Person: 0x100600230>
    NSObject class Person
    libobjc class Person
    implements methods      
    setAge:,observeValueForKeyPath:ofObject:
    change:context:,height,setHeight:>

一切都是原来的的样子,runtime的magic。

结论

只要监听了属性的改变,父类就通过isa- swizzle(isa混写),指向了子类,而狡猾的子类不仅完成了该有的set方法的重写,而且重写class方法,返回父类的类对象。然而runtime之下,无所隐藏.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Golang同步:锁的使用案例详解

互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段。它由标准库代码包sync中的Mutex结构体类型代表。只有两个公开方法 Lock Unlock ...

3367
来自专栏GreenLeaves

C# 委托

一、前言:每次看到委托和事件,心理面总是不自在,原因大家都懂,但是委托和事件在.NET FrameWork里面的应用非常的广泛,所以熟练的掌握委托和事件对一个....

1798
来自专栏芋道源码1024

注册中心 Eureka源码解析 —— 应用实例注册发现 (九)之岁月是把萌萌的读写锁

本文主要分享 Eureka 注册中心的那把读写锁,让我瘙痒难耐,却不得其解。在某次意外的抠脚的一刻( 笔者不抽烟,如果抽烟的话,此处应该就不是抠脚了 ),突然顿...

850
来自专栏码洞

如何优雅的关闭Go Channel【译】

不要在消费端关闭channel,不要在有多个并行的生产者时对channel执行关闭操作。

1073
来自专栏iOS技术

透彻理解 KVO 观察者模式(附基于runtime实现代码)

推荐另一篇文章:透彻理解 NSNotificationCenter 通知(含实现代码)

3978
来自专栏岑志军的专栏

KVO详解及底层实现

772
来自专栏林德熙的博客

win10 uwp 异步转同步 使用的条件使用方法使用Task.Wait 时需要小心死锁

在本文开始,我必须告诉大家,这个方法可能立即死锁,所以使用的时候需要满足下面的条件

662
来自专栏Golang语言社区

GO语言常用的文件读取方式

本文实例讲述了GO语言常用的文件读取方式。分享给大家供大家参考。具体分析如下: Golang 的文件读取方法很多,刚上手时不知道怎么选择,所以贴在此处便后速查。...

3907
来自专栏Golang语言社区

Golang面试题

最近在很多地方看到了golang的面试题,看到了很多人对Golang的面试题心存恐惧,也是为了复习基础,我把解题的过程总结下来。 面试题 写出下面代码输出内容。...

5409
来自专栏小勇DW3

Java设计模式-单例模式

作为对象的创建模式,单例模式确保其某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类。单例模式有以下特点:

925

扫码关注云+社区