KVO概述
KVO(Key-Value-Observing)是iOS开发中一种观察者模式实现,允许对象监听另一个对象属性的变化。当被观察属性的值发生变化时,观察者会收到通知。KVO基于NSKeyValueObserving协议实现,是Foundation框架的核心功能之一。
1.KVO的基本使用
我们以下面的demo为例,当我们滑动列表的时候,页面上显示UITableView的偏移量。
图1.KVO监听UITableView滑动时候的偏移量
具体的使用步骤如下:
1.添加观察者
通过addObserver:forKeyPath:options:context:
方法注册观察者。
observedObject.addObserver(observer, forKeyPath: "propertyName", options: [.new, .old], context: nil
)
options
:指定接收变化前的值(.old
)或变化后的值(.new
)。
context
:区分不同观察事件的上下文指针(通常用nil
)。
2.实现回调方法
观察者需重写observeValue(forKeyPath:of:change:context:)
方法处理通知:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?
) {if keyPath == "propertyName" {let oldValue = change?[.oldKey]let newValue = change?[.newKey]// 处理值变化}
}
3.移除观察者
在观察者销毁前必须调用removeObserver:forKeyPath:
observedObject.removeObserver(observer, forKeyPath: "propertyName")
4.完整的代码
完整的代码如下:
import UIKit
import IFLYCommonKit
import SnapKitclass IFLYKVODemosVC: IFLYCommonBaseVC {private let tableView = UITableView()private let offsetLabel: UILabel = {let label = UILabel()label.backgroundColor = UIColor.black.withAlphaComponent(0.6)label.textColor = .whitelabel.font = UIFont.systemFont(ofSize: 14)label.textAlignment = .centerlabel.layer.cornerRadius = 5label.clipsToBounds = truereturn label}()private var contentOffsetObservation: NSKeyValueObservation?override func viewDidLoad() {super.viewDidLoad()title = "KVO原理"// Do any additional setup after loading the view.view.addSubview(self.tableView)view.addSubview(offsetLabel)tableView.snp.makeConstraints { make inmake.edges.equalToSuperview()}offsetLabel.snp.makeConstraints { make inmake.center.equalToSuperview()make.height.equalTo(30)make.width.equalTo(180)}contentOffsetObservation = tableView.observe(\.contentOffset, options: [.new]) { [weak self] tableView, change inguard let self = self, let newOffset = change.newValue else { return }let offsetY = newOffset.yself.offsetLabel.text = String(format: "Offset Y: %.2f", offsetY)self.offsetLabel.isHidden = falseNSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.hideOffsetLabel), object: nil)self.perform(#selector(self.hideOffsetLabel), with: nil, afterDelay: 0.5)}}@objc private func hideOffsetLabel() {offsetLabel.isHidden = true}deinit {contentOffsetObservation?.invalidate()}override func viewWillAppear(_ animated: Bool) {super.viewWillAppear(animated)tableView.dataSource = selftableView.delegate = selftableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")}}extension IFLYKVODemosVC: UITableViewDataSource, UITableViewDelegate {func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {return 20}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)cell.textLabel?.text = "Row \(indexPath.row)"return cell}func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {tableView.deselectRow(at: indexPath, animated: true)print("Selected row \(indexPath.row)")}
}
2.KVO的实现原理
1.动态子类(Dynamic Subclassing)
KVO会在运行时动态生成被观察类的子类(命名规则为NSKVONotifying_ClassName
),并重写被观察属性的setter方法。
2.Setter方法重写
动态子类的setter方法会调用以下流程:
willChangeValueForKey:
:通知系统属性即将变化。- 调用原始setter方法修改属性值。
didChangeValueForKey:
:通知系统属性已变化,触发观察者回调。
3.手动触发KVO
即使未直接赋值,手动调用willChangeValueForKey:
和didChangeValueForKey:
也能触发通知:
observedObject.willChangeValue(forKey: "propertyName")
// 手动修改属性值(如直接操作实例变量)
observedObject.didChangeValue(forKey: "propertyName")
4.isa-swizzling
动态子类会修改被观察对象的isa
指针,指向新生成的子类,从而让方法调用路由到重写的逻辑。
3.KVO的注意事项
1.移除观察者
未移除观察者会导致 crash。观察者销毁前需确保移除所有观察。
2.线程安全
KVO通知与属性修改在同一线程触发,跨线程观察需谨慎处理线程同步。
3.性能优化
避免高频属性(如滚动时的UI坐标)使用KVO,频繁通知可能影响性能。
4.Swift中的使用
在Swift中需将观察对象和属性标记为@objc dynamic
:
@objc class MyClass: NSObject {@objc dynamic var value: Int = 0
}
4.KVO的替代方案
- Combine框架:适用于Swift项目,提供更现代的响应式编程支持。
- Delegate模式:适合一对一的属性监听场景。
- Property Observers(Swift):直接使用
didSet
和willSet
监听属性变化。
通过理解KVO的原理和注意事项,可以更高效地实现数据变化的监听与响应。