iOS开发中的KVO以及原理

发布于:2025-06-29 ⋅ 阅读:(22) ⋅ 点赞:(0)

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 SnapKit

class IFLYKVODemosVC: IFLYCommonBaseVC {

    private let tableView = UITableView()
    private let offsetLabel: UILabel = {
        let label = UILabel()
        label.backgroundColor = UIColor.black.withAlphaComponent(0.6)
        label.textColor = .white
        label.font = UIFont.systemFont(ofSize: 14)
        label.textAlignment = .center
        label.layer.cornerRadius = 5
        label.clipsToBounds = true
        return 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 in
            make.edges.equalToSuperview()
        }

        offsetLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.height.equalTo(30)
            make.width.equalTo(180)
        }
        
        contentOffsetObservation = tableView.observe(\.contentOffset, options: [.new]) { [weak self] tableView, change in
            guard let self = self, let newOffset = change.newValue else { return }
            let offsetY = newOffset.y
            self.offsetLabel.text = String(format: "Offset Y: %.2f", offsetY)
            self.offsetLabel.isHidden = false

            NSObject.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 = self
        tableView.delegate = self
        tableView.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方法会调用以下流程:

  1. willChangeValueForKey::通知系统属性即将变化。
  2. 调用原始setter方法修改属性值。
  3. 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):直接使用didSetwillSet监听属性变化。

通过理解KVO的原理和注意事项,可以更高效地实现数据变化的监听与响应。


网站公告

今日签到

点亮在社区的每一天
去签到