深入理解KVO

作者: CoderSpr1ngHall | 来源:发表于2019-04-17 15:31 被阅读20次

    先来说说什么是KVO,KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。

    KVO的使用

    1、注册观察者

    //注册观察者
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    

    option:

    • NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法
    • NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法
    • NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
    • NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。

    context:

    这里的context字面上面的意思是上下文。但是在实际的开发中,我们可以把它理解成为一个标记。通常是为了在类和类的子类中同时对一个属性进行监听时,为了区分两个监听,则可以在context中传入一个标识"person_name"。如果不需要传入值,则传入NULL即可。

    那么用context有什么好处呢?在我们接收属性变化的回调的时候,同时会拿到相应的keyPathobjectchangecontext。如果去判断keyPath的话,我们需要判断缓存列表,还要判断类的列表,才能找到相应的属性值。但是context可以看做是一个静态的值放在内存中,所以用context会让性能提升不少。

    2、监听回调

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

    3、移除观察者

    - (void)dealloc {    
        [self.person removeObserver:self forKeyPath:@"name"];
    }
    

    那么我们可能要思考为什么要移除观察者呢?我们再查看了官方文档之后,就能清楚的看见:

    When removing an observer, keep several points in mind:

    • Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either callremoveObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.
    • An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
    • The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.

    解除分配时,观察者不会自动删除自身。被观察对象继续发送通知,无视观察者的状态。但是发送到已发布对象的更改通知与任何其他消息一样,会去出发内存访问异常。因此,要确保观察者在从内存中消失之前将其移除。

    KVO的监听

    1、自动监听属性值

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {    
        if ([key isEqualToString:@"name"]) {        
            return YES;    
        }    
        return NO;
    }
    

    2、手动观察属性值

    - (void)setName:(NSString *)name {    
        [self willChangeValueForKey:@"name"];
        _name = name;    
        [self didChangeValueForKey:@"name"];
    }
    

    KVO原理分析

    在查看了官方文档之后,发现KVO的原理其实是改变了isa指针,将isa指针指向了另一个动态生成的类。为了分析其中的原理,我们在下图地方做一个断点,看看到底是生成了什么样的类。

    image

    然后我们用LLDB打印一下相应的类。

    image

    我们发现出来了一个新的类NSKVONotifing_Person。那么这个新的类跟Person类是什么关系呢?于是决定打印添加了observer前后类到底有什么变化。

    调用printClasses方法可以打印所有的子类的信息:

    #pragma mark - ======== 遍历类以及子类 ========
    - (void)printClasses:(Class)cls {    
        //注册类的总和    
        int count = objc_getClassList(NULL, 0);    
        //创建一个数组,其中包含给定的对象    
        NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];    
        //获取所有已注册的类    
        Class *classes = (Class *)malloc(sizeof(Class) * count);    
        objc_getClassList(classes, count);    
        for (int i = 0; i < count; i++) {        
            if (cls == class_getSuperclass(classes[i])) {            
            [mArray addObject:classes[i]];        
            }    
        }    
        free(classes);   
        NSLog(@"classes = %@",mArray);
    }
    

    然后我们再调用前和调用后分别调用方法:

        [self printClasses:[Person class]];    
        NSLog(@"*********添加前*********");    
        //[self printClasses:NSClassFromString(@"NSKOVNotifing_Person")];        
        self.person = [[Person alloc]init];    
        self.person.name = @"zy";    
        //注册观察者    
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];    
        NSLog(@"*********进去了*********");    
        [self printClasses:[Person class]];    
        NSLog(@"*********添加后:NSKVONotifying_Person*********");    
        [self printClasses:NSClassFromString(@"NSKVONotifying_Person")];
    

    打印出来的值为

    2019-04-14 20:27:48.150739+0800 KVODemo[1959:134800] classes = (

    Person

    )

    **2019-04-14 20:27:48.150926+0800 KVODemo[1959:134800] *********添加前***********

    **2019-04-14 20:27:48.151333+0800 KVODemo[1959:134800] *********进去了***********

    2019-04-14 20:27:48.157911+0800 KVODemo[1959:134800] classes = (

    Person,

    "NSKVONotifying_Person"

    )

    **2019-04-14 20:27:48.158066+0800 KVODemo[1959:134800] *********添加后:NSKVONotifying_Person***********

    2019-04-14 20:27:48.162250+0800 KVODemo[1959:134800] classes = (

    "NSKVONotifying_Person"

    )

    那么我们可以明确的看到,NSKVONotifying_Person是继承与Person的,他是动态生成的Person的子类。

    弄清楚了类的关系,我们再看看方法有什么变化。我们新增一个遍历所有方法的函数:

    #pragma mark - ======== 遍历方法-ivar-property ========
    - (void)printClassAllMethod:(Class)cls {    
        unsigned int count = 0;    
        Method *methodList = class_copyMethodList(cls, &count);    
        for (int i = 0; i < count; i++) {        
            Method method = methodList[i];        
            SEL sel = method_getName(method);        
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
        }
        free(methodList);
    }
    

    然后在添加前后分别调用printClassAllMethod方法,打印的结果为:

    2019-04-14 20:44:43.451245+0800 KVODemo[2131:144186] hello-0x10a759290

    2019-04-14 20:44:43.451427+0800 KVODemo[2131:144186] world-0x10a7592c0

    2019-04-14 20:44:43.451529+0800 KVODemo[2131:144186] nick-0x10a759320

    2019-04-14 20:44:43.451633+0800 KVODemo[2131:144186] setNick:-0x10a759350

    2019-04-14 20:44:43.451738+0800 KVODemo[2131:144186] .cxx_destruct-0x10a759390

    2019-04-14 20:44:43.451879+0800 KVODemo[2131:144186] name-0x10a7592f0

    2019-04-14 20:44:43.451963+0800 KVODemo[2131:144186] setName:-0x10a7591f0

    **2019-04-14 20:44:43.452100+0800 KVODemo[2131:144186] *********添加前***********

    **2019-04-14 20:44:43.452582+0800 KVODemo[2131:144186] *********进去了***********

    **2019-04-14 20:44:43.452672+0800 KVODemo[2131:144186] *********添加后:NSKVONotifying_Person***********

    2019-04-14 20:44:43.452786+0800 KVODemo[2131:144186] setName:-0x10aab263a

    2019-04-14 20:44:43.452884+0800 KVODemo[2131:144186] class-0x10aab106e

    2019-04-14 20:44:43.452968+0800 KVODemo[2131:144186] dealloc-0x10aab0e12

    2019-04-14 20:44:43.453067+0800 KVODemo[2131:144186] _isKVOA-0x10aab0e0a

    那么,通过对方法的地址分析,我们可以得到一个结论,NSKVONotifying_Person类重写了setName的方法,然后新增了class方法、dealloc方法和_isKVOA方法。

    结论

    综合上面的测试,我们可以总结出来KVO的原理:

    1. 验证是否存在setter方法,目的是为了不让实例进来
    2. 动态生成子类NSKVONotifying_Person:先开辟一个新的类,然后注册类,重写class的方法,讲class指向Person,接着重写setter方法,通过对setter赋值,实现父类的方法self.name = @"xlh",最后通过objc_getAssociatedObject关联住我们的观察者
    3. 讲isa的指针指向NSKVONotifying_Person
    4. 最后通过消息转发响应响应的回调

    相关文章

      网友评论

        本文标题:深入理解KVO

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