iOS开发 - KVO原理及应用场景

原文链接

简介

KVO(Key-Value Observing)是一种iOS开发中常用的机制,用于监视对象属性的变化,当被观察的对象的属性发生变化时,它会自动通知观察者做出相应的响应。

API

常用API

常见的KVO API包括:

  1. addObserver:forKeyPath:options:context::添加一个观察者,并指定要观察的属性、观察选项和上下文。
  2. removeObserver:forKeyPath:context::移除一个观察者,并指定要移除观察的属性和上下文。在观察者对象被销毁前,需要调用此方法移除观察。
  3. observeValueForKeyPath:ofObject:change:context::当观察到指定属性发生改变时,执行相应的操作。此方法需要实现在观察者对象中。
  4. keyPathsForValuesAffectingValueForKey::返回与当前属性值相关的属性路径。
  5. automaticallyNotifiesObserversForKey::指定是否自动发送通知给观察者对象。默认情况下,属性的变化会自动发送通知给观察者对象。
  6. willChangeValueForKey:didChangeValueForKey::手动触发属性变化时,可以调用这两个方法发送通知给观察者对象。

简单示例

一、属性监听

  1. 创建被监听的对象和监听者对象
class Person: NSObject {
    @objc dynamic var name: String
    @objc dynamic var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Observer: NSObject {
    var person: Person
    
    init(person: Person) {
        self.person = person
        super.init()
        
        // 添加KVO监听,person参数为被监听的对象,keyPath为要监听的属性,options为监听选项
        person.addObserver(self, forKeyPath: "name", options: [.old, .new], context: nil)
        person.addObserver(self, forKeyPath: "age", options: [.old, .new], context: nil)
    }
    
    // 实现observeValue方法,当属性变化时会自动调用
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "name" {
            print("person的name属性从\(change?[.oldKey] ?? "")变为\(change?[.newKey] ?? "")")
        } else if keyPath == "age" {
            print("person的age属性从\(change?[.oldKey] ?? "")变为\(change?[.newKey] ?? "")")
        }
    }
    
    // 移除KVO监听
    deinit {
        person.removeObserver(self, forKeyPath: "name")
        person.removeObserver(self, forKeyPath: "age")
    }
}
  1. 创建被监听的对象和监听者对象的实例,并进行测试
let person = Person(name: "张三", age: 20)
let observer = Observer(person: person)

person.name = "李四"
person.age = 30

// 输出:
// person的name属性从张三变为李四
// person的age属性从20变为30

二、属性依赖
KVO属性依赖表示一个属性的值取决于另一个属性的值。例如,如果我们有一个Rectangle类,它具有宽度和高度两个属性,那么我们可以计算出它的面积。在这种情况下,面积是依赖于宽度和高度的。我们可以使用KVO来监听宽度和高度的变化,并在变化时重新计算面积。下面是一个示例:

class Rectangle: NSObject {
    @objc dynamic var width: CGFloat = 0.0
    @objc dynamic var height: CGFloat = 0.0
    
    @objc dynamic var area: CGFloat {
        return width * height
    }
    
    override static func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if key == "area" {
            return Set(["width", "height"])
        }
        return super.keyPathsForValuesAffectingValue(forKey: key)
    }
}

let rect = Rectangle()
rect.width = 10.0
rect.height = 5.0

rect.addObserver(rect, forKeyPath: #keyPath(Rectangle.area), options: [.old, .new], context: nil)
rect.width = 20.0
rect.height = 8.0

在上面的代码中,我们定义了一个Rectangle类,它具有width、height和area属性。在area属性的getter方法中,我们计算了矩形的面积并返回它的值。在keyPathsForValuesAffectingValue(forKey:)方法中,我们指定了area属性受width和height属性的影响,当width或height属性发生变化时,我们知道area属性也会发生变化。

在代码的最后,我们创建了一个Rectangle实例,并设置width和height的值。然后我们添加了一个观察者来监听area属性的变化,并将width和height的值更新。由于我们已经指定了area属性依赖于width和height属性,当width或height属性发生变化时,area属性也会发生变化,并触发observeValue方法,打印出Area changed from xxx to xxx的信息。

使用场景

KVO的使用场景非常广泛,如以下几个方面:

  1. UI自动更新:当一个属性发生变化时,自动更新UI控件。
  2. 缓存管理:当缓存中的某个对象发生变化时,能够自动更新其他相关的对象。
  3. 监听网络请求:当网络请求中的数据发生变化时,能够自动更新界面。

UI自动更新

在UI界面中,KVO可以用于自动更新UI,下面是一个使用KVO实现UI自动更新的示例:

  1. 创建一个需要观察的对象
class TextObject: NSObject {
    @objc dynamic var text: String = ""
}
  1. 在需要观察的控制器中注册观察者
class ViewController: UIViewController {

    @objc var textObject = TextObject()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 注册观察者
        textObject.addObserver(self, forKeyPath: #keyPath(TextObject.text), options: [.new], context: nil)
    }

    // 观察者回调函数,当textObject的text属性发生变化时会被调用
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(TextObject.text) {
            // 更新UI
            label.text = textObject.text
        }
    }
}
  1. 在需要更新UI的地方改变对象的属性值
textObject.text = "Hello KVO"

这样,当textObject的text属性发生变化时,观察者的observeValue函数就会被调用,更新UI。这就是使用KVO实现UI自动更新的过程。

缓存管理

一、OC实现
使用KVO实现缓存管理的步骤如下:

  1. 创建一个缓存管理类。
  2. 在该类中定义一个可变字典,用于存储缓存数据。
  3. 在该类中添加一个观察者方法,监听需要缓存的对象属性的变化。
  4. 在观察者方法中,根据属性变化的情况来更新缓存数据。
  5. 在需要使用缓存数据的类中,注册该类为观察者。
  6. 当需要缓存的对象属性发生变化时,观察者方法会被调用,缓存数据也会被更新。
  7. 在需要使用缓存数据的地方,直接从缓存管理类中获取数据即可。

例如,以下是一个使用KVO实现缓存管理的示例代码:

// 缓存管理类
@interface CacheManager : NSObject

@property (nonatomic, strong) NSMutableDictionary *cacheData;

+ (instancetype)sharedManager;

- (void)addObserverFor:(id)object keyPath:(NSString *)keyPath;

@end

@implementation CacheManager

+ (instancetype)sharedManager {
    static dispatch_once_t onceToken;
    static CacheManager *manager = nil;
    dispatch_once(&onceToken, ^{
        manager = [[CacheManager alloc] init];
    });
    return manager;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _cacheData = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)addObserverFor:(id)object keyPath:(NSString *)keyPath {
    [object addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    id newValue = change[NSKeyValueChangeNewKey];
    if (newValue) {
        [self.cacheData setObject:newValue forKey:keyPath];
    }
}

@end

// 使用缓存数据的类
@interface MyClass : NSObject

@property (nonatomic, strong) NSString *myProperty;

@end

@implementation MyClass

- (void)dealloc {
    [[CacheManager sharedManager].cacheData removeObjectForKey:@"myProperty"];
    [self removeObserver:self forKeyPath:@"myProperty"];
}

- (instancetype)init {
    self = [super init];
    if (self) {
        [[CacheManager sharedManager] addObserverFor:self keyPath:@"myProperty"];
    }
    return self;
}

@end

// 获取缓存数据
NSString *cachedValue = [[CacheManager sharedManager].cacheData objectForKey:@"myProperty"];

二、Swift 实现
KVO(Key-Value Observing)是Objective-C中一种观察者模式的实现方式,可以用来监听对象某些属性的变化。在Swift中同样可以使用KVO,但需要注意的是被观察的属性需要用@objc修饰。

下面是一个利用KVO实现缓存管理的示例代码:

class CacheManager: NSObject {
    static let shared = CacheManager()
    private let cache = NSCache<NSString, AnyObject>()
    
    override init() {
        super.init()
        NotificationCenter.default.addObserver(self, selector: #selector(removeAllObjects), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
        // 监听UIApplication的内存警告通知,收到通知后调用removeAllObjects方法清空缓存
    }
    
    func setObject(_ object: AnyObject, forKey key: String) {
        cache.setObject(object, forKey: key as NSString)
    }
    
    func objectForKey(_ key: String) -> AnyObject? {
        return cache.object(forKey: key as NSString)
    }
    
    @objc func removeAllObjects() {
        cache.removeAllObjects()
    }
    
    // 监听cache的count属性,当其值超过100时清空缓存
    @objc dynamic var count: Int {
        if cache.totalCount > 100 {
            cache.removeAllObjects()
        }
        return cache.totalCount
    }
}

在上面的代码中,CacheManager负责管理缓存。当收到UIApplication的内存警告通知时,会调用removeAllObjects方法清空缓存。还实现了一个监听cache的count属性的方法,当count超过100时也会清空缓存。在使用这些缓存时,可以通过CacheManager单例实例来调用setObject和objectForKey方法来添加或获取缓存。

let cacheManager = CacheManager.shared
cacheManager.setObject(imageData as AnyObject, forKey: imageUrl)
let cachedImageData = cacheManager.objectForKey(imageUrl) as? Data

需要注意的是,KVO有一些限制和注意事项,比如只有继承自NSObject的类才能使用KVO。同时,为避免内存泄漏,需要在不需要监听时手动调用removeObserver方法来移除观察者。

监听网络请求

  1. 首先,需要为网络请求所在的类添加一个监听属性,用于监听数据的改变:
@interface MyNetworkService : NSObject

@property (nonatomic, strong) NSString *data; // 监听属性

- (void)requestDataWithCompletionHandler:(void(^)(NSString *data))completionHandler;

@end
  1. 在请求数据的方法中,当请求成功时,将数据赋值给监听属性,然后发送KVO通知:
@implementation MyNetworkService

- (void)requestDataWithCompletionHandler:(void(^)(NSString *data))completionHandler {
    // 发送异步网络请求
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 模拟网络请求延迟
        [NSThread sleepForTimeInterval:2];
        
        // 请求成功,获取到数据
        NSString *data = @"这里是网络请求返回的数据";
        
        // 在主线程调用completionHandler
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completionHandler) {
                completionHandler(data);
            }
            
            // 数据发生改变,发送KVO通知
            [self willChangeValueForKey:@"data"];
            self.data = data;
            [self didChangeValueForKey:@"data"];
        });
    });
}

@end
  1. 在需要监听数据改变的地方,添加KVO观察者:
@interface MyViewController : UIViewController

@property (nonatomic, strong) MyNetworkService *networkService;

@end

@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.networkService = [[MyNetworkService alloc] init];
    [self.networkService addObserver:self forKeyPath:@"data" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)dealloc {
    [self.networkService removeObserver:self forKeyPath:@"data"];
}

- (void)updateUIWithData:(NSString *)data {
    // 更新UI
}

#pragma mark - KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == self.networkService && [keyPath isEqualToString:@"data"]) {
        NSString *data = change[NSKeyValueChangeNewKey];
        [self updateUIWithData:data];
    }
}

@end
  1. 当监听属性的值发生改变时,KVO观察者会收到通知并调用observeValueForKeyPath:ofObject:change:context:方法,在其中更新UI。

这样,就可以使用KVO监听网络请求,当网络请求中的数据发生改变时,更新界面了。

原理

KVO的底层原理如下:

  1. 在注册观察者之前,运行时系统会创建一个隐藏的派生类,该类重写被观察对象的setter方法。
  2. 向被观察对象的isa指针指向的新类对象发送addObserver:forKeyPath:options:context:消息。
  3. 在接收到该消息后,KVO机制会在被观察对象的一个私有属性里记录下观察者和被观察属性的一些信息。
  4. 当被观察属性发生变化时,KVO机制会重写派生类的setter方法,在调用原setter方法之前,会通知所有观察者发生变化。
  5. 观察者接收到通知后,可以通过回调方法observeValueForKeyPath:ofObject:change:context:获取被观察属性的新值和旧值,以及其他一些变化信息。

需要注意的是,使用KVO时应遵循以下规则:

  1. 被观察对象的属性必须是Objective-C对象类型,而不能是基本类型或C数据类型。
  2. 观察者需要实现observeValueForKeyPath:ofObject:change:context:方法来接收通知,在方法中根据变化的键路径和上下文进行处理。
  3. 观察者退出时,应调用被观察对象的removeObserver:forKeyPath:方法来移除观察者,并避免内存泄漏。

小结

KVO是一种方便灵活的编程方式,可以在不修改代码的情况下对对象的属性进行观察和响应。但是需要注意,KVO并不适用于所有情况,有些场景下可能需要使用其他的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值