iOS开发基础15-KVO属性监听与底层原理

KVO(Key-Value Observing)深度解析:属性监听与底层原理

KVO 是 Objective-C 运行时提供的属性监听机制,允许对象在其他对象的属性发生变化时接收通知。它是响应式编程、数据绑定和许多 UIKit 组件(如 UISliderUISwitch)的底层基础。


一、KVO 的使用

1. 基本用法

定义被观察对象

// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

// Person.m
@implementation Person
@end

注册监听

@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.person.name = @"chg";
    self.person.age = 30;

    // 注册 KVO 监听
    [self.person addObserver:self
                  forKeyPath:@"age"
                     options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                     context:nil];
}

#pragma mark - KVO 回调

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"Age changed: %@ → %@",
              change[NSKeyValueChangeOldKey],
              change[NSKeyValueChangeNewKey]);
    }
}

- (void)dealloc {
    // 必须移除监听,否则被观察对象释放时可能崩溃
    [self.person removeObserver:self forKeyPath:@"age"];
}

@end

2. options 参数详解

选项 说明
NSKeyValueObservingOptionNew change 字典包含新值(NSKeyValueChangeNewKey
NSKeyValueObservingOptionOld change 字典包含旧值(NSKeyValueChangeOldKey
NSKeyValueObservingOptionInitial 注册时立即回调一次(传递初始值)
NSKeyValueObservingOptionPrior 值改变前回调一次(change[NSKeyValueChangeNotificationIsPriorKey] = YES),改变后再回调一次

3. context 参数的最佳实践

context 是一个 void * 指针,用于区分不同的观察关系。当父类也注册了 KVO 时,使用唯一的 context 可以避免回调冲突:

// 定义唯一的 context 指针(取静态变量的地址)
static void *PersonAgeContext = &PersonAgeContext;
static void *PersonNameContext = &PersonNameContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:PersonAgeContext];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:PersonNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == PersonAgeContext) {
        NSLog(@"age 变化: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
    } else if (context == PersonNameContext) {
        NSLog(@"name 变化: %@ → %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
    } else {
        // 不属于自己的 context,交给父类处理
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

4. 手动触发 KVO

默认情况下,KVO 自动在 setter 中插入通知。如果需要手动控制通知时机(如批量修改后只通知一次),可以关闭自动通知并手动触发:

// 关闭 age 属性的自动通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setAge:(int)age {
    if (_age != age) {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }
}

willChangeValueForKey: 必须在值改变前调用,didChangeValueForKey: 在值改变后调用,两者必须成对出现。

5. 键路径依赖

当一个属性的值依赖于其他属性时,可以通过 keyPathsForValuesAffectingValueForKey: 声明依赖关系,使被依赖属性变化时也触发当前属性的 KVO:

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy, readonly) NSString *fullName; // 只读,依赖 firstName 和 lastName
@end

@implementation Person

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

// fullName 的值依赖于 firstName 和 lastName
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}

@end

此时监听 fullName,当 firstNamelastName 变化时也会收到通知。

6. 注意事项

  • 必须移除监听:观察者释放前或被观察对象释放前,必须调用 removeObserver:forKeyPath:。iOS 11+ 对部分 KVO 崩溃做了保护,但最佳实践仍是手动移除。
  • 只能监听 setter 修改:直接访问实例变量(如 _age = 30)不会触发 KVO,必须通过 setter 或 KVC(setValue:forKey:)修改。
  • 线程安全:KVO 回调在哪个线程修改属性就在哪个线程触发,不保证在主线程。UI 操作需手动切回主线程。

二、Swift 中的 KVO

1. 传统方式(基于 OC 运行时)

Swift 类使用 KVO 必须满足:

  • 类继承自 NSObject
  • 属性标记 @objc dynamic
class Person: NSObject {
    @objc dynamic var name: String = ""
    @objc dynamic var age: Int = 0
}

class ViewController: UIViewController {
    var person: Person!

    override func viewDidLoad() {
        super.viewDidLoad()
        person = Person()
        person.addObserver(self, forKeyPath: "age", options: [.old, .new], context: nil)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                               change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "age" {
            print("Age changed: \(change?[.oldKey] ?? "") → \(change?[.newKey] ?? "")")
        }
    }

    deinit {
        person.removeObserver(self, forKeyPath: "age")
    }
}

2. Swift 4+ Block 方式(推荐)

Swift 4 引入了类型安全的 block-based KVO API,返回 NSKeyValueObservation 对象,自动管理生命周期(对象释放时自动移除监听):

class ViewController: UIViewController {
    var person: Person!
    var observation: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()
        person = Person()

        observation = person.observe(\.age, options: [.old, .new]) { person, change in
            print("Age changed: \(change.oldValue ?? 0) → \(change.newValue ?? 0)")
        }
    }
    // observation 释放时自动移除监听,无需手动 removeObserver
}

3. Combine 框架(iOS 13+,现代替代方案)

Combine 是 Apple 官方的响应式编程框架,@Published 提供了比 KVO 更优雅的属性观察:

import Combine

class Person: ObservableObject {
    @Published var age: Int = 0
}

class ViewController: UIViewController {
    let person = Person()
    var cancellable: AnyCancellable?

    override func viewDidLoad() {
        super.viewDidLoad()
        cancellable = person.$age.sink { newValue in
            print("Age changed to \(newValue)")
        }
    }
}

三、KVO 底层原理

1. ISA-Swizzling 与动态子类

KVO 的核心是 ISA-Swizzling(而非 Method Swizzling):

  1. 当第一次为对象的某个属性注册 KVO 时,系统在运行时动态创建一个名为 NSKVONotifying_原类名 的子类(如 NSKVONotifying_Person)。
  2. 将被观察对象的 isa 指针从原类修改为这个动态子类(ISA-Swizzling)。
  3. 动态子类重写被监听属性的 setter 方法,在其中插入 willChangeValueForKey:didChangeValueForKey: 调用。
  4. 动态子类还重写了以下方法来隐藏自身存在:
    • class:返回原类(而非动态子类),欺骗外部调用者。
    • dealloc:释放 KVO 相关资源。
    • _isKVOA:内部标记方法,返回 YES

ISA-Swizzling vs Method Swizzling:Method Swizzling 是交换两个已有方法的实现;ISA-Swizzling 是修改对象的 isa 指针指向一个新创建的动态子类。两者是不同的技术。

2. 被重写的 setter

动态子类中 setAge: 的实现大致如下:

- (void)setAge:(int)age {
    [self willChangeValueForKey:@"age"];
    [super setAge:age]; // 调用原类的 setter
    [self didChangeValueForKey:@"age"];
}

didChangeValueForKey: 内部会遍历所有注册的观察者,调用其 observeValueForKeyPath:ofObject:change:context: 方法。

3. 验证动态子类存在

可以通过以下代码验证 KVO 的动态子类机制:

NSLog(@"注册前: %s", object_getClassName(self.person));
// 输出: Person

[self.person addObserver:self forKeyPath:@"age" options:0 context:nil];

NSLog(@"注册后: %s", object_getClassName(self.person));
// 输出: NSKVONotifying_Person

NSLog(@"class 方法返回: %@", [self.person class]);
// 输出: Person(class 方法被重写,隐藏了动态子类)

4. 完整通知流程

调用 setter → willChangeValueForKey: → 记录旧值 → 调用原 setter 赋值
→ didChangeValueForKey: → 遍历观察者 → 调用 observeValueForKeyPath:...

四、总结

维度 说明
用途 监听对象属性变化,实现数据绑定和响应式更新
触发条件 通过 setter 或 KVC 修改属性(直接访问实例变量不触发)
底层原理 ISA-Swizzling + 动态子类 NSKVONotifying_XXX 重写 setter
必须移除 观察者/被观察者释放前移除监听,避免崩溃
Swift 要求 继承 NSObject + @objc dynamic 属性
Swift 推荐 observe(_:options:changeHandler:) block API 或 Combine
  • KVO 是 Objective-C 运行时的特性,基于 ISA-Swizzling 实现,并非 Method Swizzling。
  • 使用 context 指针区分不同观察关系,避免父类子类回调冲突。
  • 手动控制通知时需成对调用 willChangeValueForKey: / didChangeValueForKey:
  • Swift 新项目优先使用 Combine 的 @Published,兼容性要求高时使用 block-based KVO。

posted @ 2015-07-22 00:38  Mr.陳  阅读(303)  评论(0)    收藏  举报