不为人知的iOS KVO潜力

764 阅读3分钟

概述

在 iOS 开发中,KVO(Key-Value Observing)是 iOS 中一种强大的观察者模式实现,它允许对象监听其他对象特定属性的改变。当被观察的属性发生变化时,观察者会收到相应的通知。本文将对KVO 的原理、使用方式、注意事项以及优缺点深入探讨。

KVO核心机制

KVO本质是通过 isa-swizzling技术实现的,具体步骤如下

1.动态子类创建

  • 当某个对象第一次被观察时,运行时系统会动态创建该对象类的子类,子类命名通常为 NSKVONotifying_原类名
  • NSKVONotifying_原类名 重写观察属性的setter方法以及dellocclass_isKVOA方法等

2.settter方法重写

  • 在赋值前调用 willChangeValueForKey
  • 在赋值后调用 didChangeValueForKey
// KVO 的伪代码实现
- (void)setName:(NSString *)name {
    // 1.通知属性即将变化
    [self willChangeValueForKey:@"name"];
    // 2.调用原始的 setter 方法
    _name = name;
    // 3.通知属性已经变化
    [self didChangeValueForKey:@"name"];
}
  • 子类重写class方法是为了屏蔽本身,隐藏其存在,不让开发者知道
  • 当修改实例对象的属性时,会调用Foundation_NSSetXXXValueAndNotify函数

3.修改isa指针

  • 运行时系统会将原对象的 isa 指针指向新创建的子类(NSKVONotifying_MyClass
  • 通过修改 isa 指针,对象的实际类型变为子类,从而使得调用 setter 方法时,会优先调用子类中重写的 setter 方法

使用方式

Objective-C

@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person
@end

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建 Person 对象
    self.person = [[Person alloc] init];
    self.person.age = 20;
    
    // 注册 KVO
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    
    // 修改 age 属性的值
    self.person.age = 30;
}

// 实现观察者的回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSInteger oldValue = [[change objectForKey:NSKeyValueChangeOldKey] integerValue];
        NSInteger newValue = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
        NSLog(@"Person's age changed from %ld to %ld", oldValue, newValue);
    }
}

// 在不需要监听属性变化时,我们应该及时移除 KVO
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    // 移除 KVO
    [self.person removeObserver:self forKeyPath:@"age"];
}

@end

Swift

class Person: NSObject {

    @objc dynamic var age: Int
    
    init(age: Int) {
        self.age = age
    }
}

class ViewController: UIViewController {

    private var person = Person(age: 20)
    private var ageObserver: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()
        // 注册 KVO
        ageObserver = person.observe(\.age, options: [.old, .new], changeHandler: { [weak self] (person, change) in
            guard let self = self else { return }
            if let oldValue = change.oldValue, let newValue = change.newValue {
                print("Person's age changed from \(oldValue) to \(newValue)")
            }
        })
        
        // 修改 age 属性的值
        person.age = 30
    }
}

func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    
    ageObserver?.invalidate()
    ageObserver = nil
}

注意,为了在Swift中使用KVO,我们需要在age属性前加上@objc dynamic修饰符。这样才能使age属性具有动态派发的特性,从而支持KVO

KVO “搞笑”时刻

场景一: 崩溃之王

KVO的经典翻车操作:忘记移除观察者,导致观察者释放后收到通知,直接野指针崩溃

// 手滑少写一句 removeObserver,直接喜提崩溃
[object addObserver:self forKeyPath:@"value" options:NSKeyValueObservingOptionNew context:nil];
// 忘记解除,崩溃警告!
// [object removeObserver:self forKeyPath:@"value"]; 

场景二:神秘失踪的通知

直接修改实例变量(不用setter)时,KVO完全无感,仿佛在演哑剧

// KVO:我什么都没看到,别问我
_value = 100; 

场景三:多线程迷惑行为

KVO通知可能在任意线程出发,开发者如果忘记切回主线程更新UI,界面直接卡成PPT

优缺点

优点

可以实现对象之间的解耦,一个对象可以监听另一个对象的属性改变,从而及时更新自己的状态。此外,KVO 还可以实现对象的属性验证和数据过滤等功能。

缺点

  • 需要手动移除观察者,容易导致内存泄漏
  • 回调方法比较混乱,需要手动判断该KeyPath
  • 不方便调试