Swift4 - KVO的浅析

KVO

KVO即Key-Value-Observing,键值观察,是观察者模式的一种实现。KVO提供了一种机制能够方便的观察对象的属性。如:指定一个被观察对象,当对象的某个属性发生变化时,对象会获得通知,进而可以做出相应的处理。在实际的开发中对于model与controller之间进行交流是非常有用的。

KVO实现原理

官方文档具体描述如下:

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

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. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

KVO的实现依赖于OC强大的Runtime,上面文档也说道,KVO是通过使用isa-swizzling技术实现的。基本的流程是:当某个类的属性对象第一次被观察时,系统会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法,派生类在被重写的setter方法内实现真正的通知机制。

例如:当观察对象A时,KVO机制动态创建一个新类,名为NSKVONotifying_A。该类继承自对象A的类,并且重写了观察属性的setter方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察者对象属性值的改变情况。

每个类对象都有一个isa指针,该指针指向当前类,当一个类对象的属性第一次被观察,系统会偷偷将isa指针指向动态生成的派生类,从而在给被监听属性赋值时执行的是派生类的setter方法。

派生类setter方法:

KVO的建值观察通知机制依赖于NSObject的两个方法,willChangeValueForKey:和didChangeValueForKey:,在进行存取数值前后分别调用这2个方法(因为我们所监的对象属性可以获取新值和旧值,所以属性改变前后都会通知观察者)。被观察属性发生改变之前,willChangeValueForKey:方法被调用,当属性发生改变之后,didChangeValueForKey:方法会被调用,之后observeValueForKey:ofObject:change:context方法也会被调用。这里要注意重写观察属性的setter方法是在运行时由系统自动执行的。而且苹果还重写了class方法,让我们误认为是使用了当前类,从而达到隐藏生成的派生类。为了帮助理解过程,借用图片一张:



KVO使用流程

1、Register the observer with the observed object using the method addObserver:forKeyPath:options:context:.
2、Implement observeValueForKeyPath:ofObject:change:context: inside the observer to accept change notification messages.
3、Unregister the observer using the method removeObserver:forKeyPath: when it no longer should receive messages. At a minimum, invoke this method before the observer is released from memory.

即:

1)调用addObserver:forkeypath:options:context方法添加观察者

2)实现observeValueForKeyPath:ofObject:change:context方法接收属性变化的通知

3)记得去除观察者,调用removeObserver:forkeypath:context方法,OC语言在dealloc方法中执行,Swift在deinit方法中,注意:有添加观察者才执行去除观察者,如果没有添加观察者,就调用去除观察者会出现异常,即添加观察者和去除观察者两个操作是一一对应的。更多详情可以看这里

一般KVO奔溃的原因

1)被观察的对象被销毁掉了,比如:被观察的对象是一个局部变量

2)观察者被释放掉了,但是没有移除监听。这个是很常见的错误,在push、pop等相关操作之后很容易崩溃

3)注册的监听没有被移除掉,又重写进行注册


使用场景

基本使用

我们创建一个简单的Person类,使用KVO监听属性name的变化,文件非常简单,只有name属性

class Person: NSObject {
    @objc dynamic var name: String = "Jack"
}

在视图控制器中,我们使用常规用法添加观察者进行监听,对于Swift4中KVC和KVO的使用介绍可以看这篇文章,官方也提供了一个简单例子

private var myContext = 0

class ViewController: UIViewController {

    let person = Person()

    override func viewDidLoad() {
        super.viewDidLoad()
        person.addObserver(self, forKeyPath: "name", options: [.new, .old], context: &myContext)
        person.name = "shihua"
    }

    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {
        if context == &myContext {
            guard let key = keyPath,
                let change = change,
                let newValue = change[.newKey] as? String,
                let oldValue = change[.oldKey] as? String else { return }
            if key == "name" {
                print("newValue: \(newValue), oldValue: \(oldValue)")
                // newValue: shihua, oldValue: Jack
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    deinit {
        person.removeObserver(self, forKeyPath: "name", context: &myContext)
    }
}

由代码可知,操作非常简单,在重新设置了person的name属性之后会触发通知,在接受通知的方法中我们打印了新值和旧值。

属性依赖

对于Person类,类里有3个属性,fullName,firstName,lastName,fullName属性值是通过firstName和firstName这两个属性值生成的,所以当这两个属性发生改变时,也相当于fullName发生改变了。如果我们要监听fullName属性的改变,我们就需要设置属性之间的依赖关系,这里有2种方案实现,具体详情看这里

使用一下,首先创建Person类,如下:

 @objcMembers class Person: NSObject {
   dynamic var firstName: String = ""
   dynamic var lastName: String = ""
   dynamic var fullName: String {
        return "\(firstName)\(lastName)"
    }

    override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if key == "fullName" {
           return Set(arrayLiteral: "firstName", "lastName")
        }
       return super.keyPathsForValuesAffectingValue(forKey: key)
    }
}

视图控制器监听fullName属性变化,先后设置firstName,lastName,会触发两次通知

private var myContext = 0

class ViewController: UIViewController {

    let person = Person()

    override func viewDidLoad() {
        super.viewDidLoad()
        person.addObserver(self, forKeyPath: "fullName", options: [.new, .old], context: &myContext)
        person.firstName = "Long"
        person.lastName = "shihua"
    }

    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {
        if context == &myContext {
            guard let key = keyPath,
                let change = change,
                let newValue = change[.newKey] as? String,
                let oldValue = change[.oldKey] as? String else { return }
            if key == "fullName" {
                print("newValue: \(newValue), oldValue: \(oldValue)")
                // newValue: Long, oldValue:
                // newValue: Longshihua, oldValue: Long
            }
        } else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }

    deinit {
        person.removeObserver(self, forKeyPath: "fullName", context: &myContext)
    }
}

手动键值观察

KVO默认情况下,只要我们为对象的某个属性添加了监听,当属性发生改变的时候就会自动的通知监听者。但是有些情况下,我们也希望手动处理这些通知,KVO为我们提供了方法,重写类方法automaticallyNotifiesObservers方法。照样操作Person类,对于firstName我们手动发送通知。

@objcMembers class Person: NSObject {
    fileprivate var _firstName: String = ""
    dynamic var firstName: String {
        set {
            willChangeValue(forKey: "firstName")
            _firstName = newValue
            didChangeValue(forKey: "firstName")
        }
        get {

            return _firstName
        }
    }

    override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
        return key == "firstName" ? false: true
    }
}


参考:

Key-Value Observing Programming

KVO和KVC的使用和实现



猜你喜欢

转载自blog.csdn.net/longshihua/article/details/79886074