kvo swift

2016-07-18 09:55:25 mydo 阅读数 1798

不像Objective-c中的类,Swift类对于KVO并没有原生的支持,不过你可以在类型安全的前提下使用属性观察者轻松的完成相同的目标.

不管如何,从NSObject类派生出的类是支持KVO的,如果你想在不使用属性观察者的情况下使用KVO,你只要从NSObject类中继承你的类.

不幸的是,即使从NSObject类派生,KVO也不是自动开启的,你希望用KVO观察的属性必须用dynamic关键字标记,才可以字Swift中的类中使用KVO.

2019-09-18 14:23:52 songzhuo1991 阅读数 235

dynamic

  • 被@objc dynamic 修饰的内容会具有动态性,比如调用方法会走runtime那一套流程
class Dog: NSObject {
    @objc dynamic func test1() {}
    func test2() {}
}
var d = Dog()
d.test1()
d.test2()
  • 对应汇编
    在这里插入图片描述
    在这里插入图片描述

KVC\KVO

  • Swift支持KVC\KVO的条件
  • 属性所在的类、监听器最终继承自NSObject
  • 用@objc dynamic 修饰对应的属性
class Observer: NSObject {
    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
        change: [NSKeyValueChangeKey: Any]?,
        context: UnsafeMutableRawPointer?) {
        print("observeValue", change?[.newKey] as Any)
    }
}
class Person: NSObject {
    @objc dynamic var age: Int = 0
    var observer: Observer = Observer()
    override init() {
        super.init()
        self.addObserver(observer,
                         forKeyPath: "age",
                         options: .new,
                         context: nil)
    }
    deinit {
        self.removeObserver(observer,
                            forKeyPath: "age")
    }
}
var p = Person()
// observeValue Optional(20)
p.age = 20
// observeValue Optional(25)
p.setValue(25, forKey: "age")

block方式的KVO

class Person: NSObject {
    @objc dynamic var age: Int = 0
    var observation: NSKeyValueObservation?
    override init() {
        super.init()
        observation = observe(\Person.age, options: [.new, .old]) {
            (person, change) in
            print(change.newValue as Any)
        }
    }
}
var p = Person()
// Optional(20)
p.age = 20
// Optional(25)
p.setValue(25, forKey: "age")
2018-07-17 08:58:42 weixin_33708432 阅读数 107

Swift是没有KVO模式的

在iOS开发中,运用oc的runtime的动态分发机制,通过key来监听value的值,达到实现KVO监听的效果。然而在Swift中是没有KVO模式的(换句话说是不能直接使用KVO模式),使用的条件必须是继承自NSObject,属性前加上dynamic来开启运行时,或者是在class前面加@objcMembers,所有的属性都会开启runtime,允许监听属性的变化。

class Car: NSObject {

    dynamic var carName: String
    
    init(carName: String) {
        self.carName = carName;
    }
    
    
}

// ************或者加@objcMembers

@objcMembers
class Car: NSObject {

    var carName: String
    
    init(carName: String) {
        self.carName = carName;
    }
    
    
}
复制代码

然后你就可以愉快的像使用oc一样来玩耍KVO了,具体实现过程就不演示了,毕竟你们都是老司机了。

KVO的实现原理

基于oc的runtime的动态分发机制,属性对象被监听时,会在运行时创建一个派生类,并重写了被观察属性keyPath的setter方法,setter方法随后负责通知观察对象属性的改变状况。

OC中的KVO及其KVO的基础知识可参见: 深入runtime探究KVO

KVO的实现原理与具体应用

Swift中属性观察器

Swift有两个属性观察者willSet和didSet,类似于触发器。用来监视属性的除初始化之外的属性值变化,当属性值发生改变时可以对此作出响应。

有如下特点:

  • 不仅可以在属性值改变后触发didSet,也可以在属性值改变前触发willSet
  • 给属性添加观察者必须要声明清楚属性类型,否则编译器报错
  • willSet可以带一个newName的参数,没有的话,该参数默认命名为newValue
  • didSet可以带一个oldName的参数,表示旧的属性,不带的话默认命名为oldValue
  • 属性初始化时,willSet和didSet不会调用。只有在初始化上下文之外,当设置属性值时才会调用
  • 即使是设置的值和原来值相同,willSet和didSet也会被调用
 var firstName: String = "Fist" {
       willSet {   //新值设置之前被调用,在此可以进行条件筛选,过滤数据
           print("willSet的新值是\(newValue)")
       }
       didSet { //新值设置之后立即调用,在此可以进行条件筛选,过滤数据,可以直接绑定数据到UI上面
           print("didSet的旧值是\(oldValue) --- 新值是 \(firstName)")
           
           self.nameLabel.text = firstName
       }
   }
复制代码

用得更多的是在didSet中跟UI绑定,或者条件逻辑过滤。

Swift实现KVO值监听

利用设计模式中的观察者模式,观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。 观察者模式适用在一个被观察者(数据源)要通知多个观察者的场景。

Swift中实现KVO,还是用到了属性观察者didSet,直接贴代码:

public class Observable<Type> {
    
    // MARK: - Callback
    fileprivate class Callback {
        fileprivate weak var observer: AnyObject?
        fileprivate let options: [ObservableOptions]
        fileprivate let closure: (Type, ObservableOptions) -> Void
        
        fileprivate init(
            observer: AnyObject,
            options: [ObservableOptions],
            closure: @escaping (Type, ObservableOptions) -> Void) {
            
            self.observer = observer
            self.options = options
            self.closure = closure
        }
    }
    
    // MARK: - Properties  利用Swift 的didSet 特性把值回调给callback
    public var value: Type {
        didSet {
            removeNilObserverCallbacks()
            notifyCallbacks(value: oldValue, option: .old)
            notifyCallbacks(value: value, option: .new)
        }
    }
    
    private func removeNilObserverCallbacks() {
        callbacks = callbacks.filter { $0.observer != nil }
    }
    
    // MARK: 回调给callback 实现闭包回调
    private func notifyCallbacks(value: Type, option: ObservableOptions) {
        let callbacksToNotify = callbacks.filter { $0.options.contains(option) }
        callbacksToNotify.forEach { $0.closure(value, option) }
    }
    
    // MARK: - Object Lifecycle
    public init(_ value: Type) {
        self.value = value
    }
    
    // MARK: - Managing Observers
    private var callbacks: [Callback] = []
    
    
    /// 添加观察者
    ///
    /// - Parameters:
    ///   - observer: 观察者
    ///   - removeIfExists: 如果观察者存在需要移除
    ///   - options: 被观察者
    ///   - closure: 回调
    public func addObserver(
        _ observer: AnyObject,
        removeIfExists: Bool = true,
        options: [ObservableOptions] = [.new],
        closure: @escaping (Type, ObservableOptions) -> Void) {
        
        if removeIfExists {
            removeObserver(observer)
        }
        
        let callback = Callback(observer: observer, options: options, closure: closure)
        callbacks.append(callback)
        
        if options.contains(.initial) {
            closure(value, .initial)
        }
    }
    
    public func removeObserver(_ observer: AnyObject) {
        callbacks = callbacks.filter { $0.observer !== observer }
    }
}

// MARK: - ObservableOptions
public struct ObservableOptions: OptionSet {
    
    public static let initial = ObservableOptions(rawValue: 1 << 0)
    public static let old = ObservableOptions(rawValue: 1 << 1)
    public static let new = ObservableOptions(rawValue: 1 << 2)
    
    public var rawValue: Int
    
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}

复制代码

以上代码出自 设计模式(Swift) - 3.观察者模式、建造者模式 这一篇文章中,欲知详情请移步查看。

使用列子:

class ViewController: UIViewController {
    // 用来管理观察者
    public class Observer {}
    
    var observer: Observer? // 当observer置为nil的时候,可观察对象会自动释放.
    let user = User(name: "yihai", info: ["name" : "Lewis"],person: Person(name: "lewis", age: 18))
   
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
       
        
        observer = Observer()
        user.name.addObserver(observer!, options: [.new]) { name, change in
            print("name:\(name), change:\(change)")
        }
        user.info.addObserver(observer!, options: [.new]) { info, change in
            print("info:\(info), change:\(change)")
        }
        
         user.person.addObserver(observer!, options: [.new]) { person, change in
            print("person:\(person), change:\(change)")
        }

    
    }


       override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        user.name.value = "Amel"
        user.info.value = ["info":"lewis info"]
        user.person.value = Person(name: "swift", age: 19)
        user.person.value.name = "我们不一样"
        user.info.value["car"] = "宝马"
        user.info.value.removeAll()
    }


}

public class User {
    // 被观察的属性需要是Observable类型
    public let name: Observable<String>
    public var info: Observable<Dictionary<String, Any>>
    public var person: Observable<Person>
    
    public init(name: String,info: Dictionary<String, Any>,person: Person) {
        self.name = Observable(name)
        self.info = Observable(info)
        self.person = Observable(person)
    }
}

public struct Person {
    var name: String
    var age: Int
    
    public init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}



}
复制代码

注意

  • 可以监听任意类型的对象
  • Dictionary的增删改支持,struct的改变某个内部属性值也可以
  • 有可能当observer置不为nil的时候,可观察对象收不到监听回调

那么对于数组Array的增删改是否一样也能实现呢?这个问题留给大神们亲自求索,我太菜了,哈哈。

第三方框架实现值监听

更方便,更简洁,更优雅

        /*  // RxSwift 实现方式  需要导入如下几个框架
            import RxSwift
            import RxCocoa
            import RxGesture
            import NSObject_Rx
        */
        

        let variable = Variable(Int())
        let variableDisposeBag = DisposeBag()

        variable.asObservable().subscribe { (number) in
                print("\(String(describing: number.element))")
            }.disposed(by: variableDisposeBag)

        variable.value = 100

复制代码

本人很喜欢RxSwift框架,后续会爬一爬它的内部实现原理。

最后

写得不是很好,请担待并提出您的宝贵意见。 希望我的文章能成为你的盛宴,也渴望你的建议能成为我的大餐。 如有错误请留言指正,对文章感兴趣可以关注作者不定期更新。

2017-11-30 20:11:57 loveiosolovehua 阅读数 2572

直接上代码

import UIKit

class ViewController: UIViewController {
    let per = Person()
    override func viewDidLoad() {
        super.viewDidLoad()
        /// 添加观察者
        per.addObserver(self, forKeyPath: "name", options: [.new,.old], context: nil)
    }
    // 点击屏幕通过kvc给Person里面的name赋值
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let num = Int(arc4random()%10000) + 1
        per.setValue("\(num)", forKey: "name")
    }
    /// KVO监听属性
    ///
    /// - Parameters:
    ///   - keyPath: 被监听的属性名
    ///   - object:
    ///   - change:
    ///   - context:
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print(keyPath ?? "")
        print("完成回调 -- \(String(describing: per.value(forKey: keyPath!)))")
    }
}
import UIKit
class Person: NSObject {
    var name:String?
}
2018-04-12 14:53:10 longshihua 阅读数 4017

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的使用和实现



Swift4.2 KVO监听

阅读数 110

Swift - KVO初探

阅读数 33