kvo整理

1.概述

KVO,即:Key-Value Observing,是 Objective-C 对 观察者模式(Observer Pattern)的实现。它提供一种机制,当指定的对象的属性被修改后,观察者就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。

2.使用

1. 基本使用

KVO本质上是基于runtime的动态分发机制,通过key来监听value的值。
OC能够实现监听因为都遵守了NSKeyValueCoding协议。OC所有的类都是继承自NSObject,其默认已经遵守了该协议,但Swift不是基于runtime的。Swift中继承自NSObject的属性处于性能等方面的考虑,默认是关闭动态分发的, 所以无法使用KVO,只有在属性前加 @objc dynamic 才会开启运行时,允许监听属性的变化。

在Swift3中只需要加上dynamic就可以了,而Swift4以后则还需要@objc

  • 注册
- (void)addObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(void *)context;

observer:观察者,也就是KVO通知的订阅者。订阅着必须实现。

keyPath:描述将要观察的属性,相对于被观察者。

options:KVO的一些属性配置;有四个选项。

NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
  • 监听
    在观察者内重写这个方法。在属性变化时,观察者则可以在函数内对属性变化做处理。
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
  • 移除

在不用的时候,不要忘记解除注册,否则会导致内存泄露。

- (void)removeObserver:(NSObject *)observer 
                forKeyPath:(NSString *)keyPath;
  • 举例
class ObservedClass: NSObject {
    // 开启运行时,允许监听属性的变化
    @objc dynamic var name: String = "Original"
    // age 并不会触发KVO
    var age: Int = 18
}

class ViewController: UIViewController {
    var observed = ObservedClass()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        observed.addObserver(self, forKeyPath: "age", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        // 修改属性值,触发KVO
        observed.name = "JiangT"
        observed.age = 22
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("属性改变了")
        print(keyPath)
        print("change字典为:")
        print(change)
    }
}

---输出结果---
属性改变了
Optional("name")
change字典为:
Optional(
[__C.NSKeyValueChangeKey(_rawValue: new): JiangT, 
__C.NSKeyValueChangeKey(_rawValue: kind): 1, 
__C.NSKeyValueChangeKey(_rawValue: old): Original])
2. 手动KVO 及禁用KVO
  1. 首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了。
  2. 其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
  3. 如果需要禁用该类KVO的话直接automaticallyNotifiesObserversForKey返回NO,实现属性的 setter 方法,不进行调用willChangeValueForKey: 和 didChangeValueForKey方法。

主要方法:

open func willChangeValue(forKey key: String)

open func didChangeValue(forKey key: String)

class func automaticallyNotifiesObservers(forKey key: String) -> Bool

举例

---被观察类---
class ObservedClass: NSObject {
 
    private var _name: String = "Original"
    @objc dynamic var name: String {
        get {
            return _name
        }
        set (n) {
            self.willChangeValue(forKey: "name")
            _name = n
            self.didChangeValue(forKey: "name")
        }
    }
    
    override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
        // 设置对该 key 不自动发送通知
        if key == "name" {
            return false
        }
        return super.automaticallyNotifiesObservers(forKey: key)
    }
}

class ViewController: UIViewController {
    var observed = ObservedClass()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        // 修改属性值,触发KVO
        observed.name = "JiangT"
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("属性改变了")
        print(keyPath)
        print("change字典为:")
        print(change)
    }
}

---输出结果---
属性改变了
Optional("name")
change字典为:
Optional([__C.NSKeyValueChangeKey(_rawValue: kind): 1, 
__C.NSKeyValueChangeKey(_rawValue: old): Original, 
__C.NSKeyValueChangeKey(_rawValue: new): JiangT])

3. 实现原理

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

大致意思为:
苹果使用了一种isa交换的技术,当ObjectA的被观察后,ObjectA对象的isa指针被指向了一个新建的子类NSKVONotifying_ObjectA,且这个子类重写了被观察值的setter方法和class方法,dealloc和isKVO方法,然后使ObjectA对象的isa指针指向这个新建的类,然后事实上ObjectA变为了NSKVONotifying ObjectA的实例对象,执行方法要从这个类的方法列表里找。

  • KVO是基于runtime机制实现的。

  • 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(如果原类为ObservedClass,那么生成的派生类名为NSKVONotifying_ObservedClass),在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制

  • 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类(isa-swizzling,后续Runtime学习记录中展开),从而在给被监控属性赋值时执行的是派生类的setter方法。派生类中还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类。
测试代码
@interface KVOObject : NSObject
@property (nonatomic, copy  ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation KVOObject

- (NSString *)description {
    NSLog(@"object address : %p \n", self);
    
    IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
    IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
    NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
    
    Class objectMethodClass = [self class];
    Class objectRuntimeClass = object_getClass(self);
    Class superClass = class_getSuperclass(objectRuntimeClass);
    NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
    
    NSLog(@"object method list \n");
    unsigned int count;
    Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"method Name = %@\n", methodName);
    }
    
    return @"";
}

在另一个类中分别创建两个KVOObject对象,其中一个对象被观察者通过KVO的方式监听,另一个对象则始终没有被监听。在KVO前后分别打印两个对象的关键信息,看KVO前后有什么变化。

@property (nonatomic, strong) KVOObject *object1;
@property (nonatomic, strong) KVOObject *object2;

self.object1 = [[KVOObject alloc] init];
self.object2 = [[KVOObject alloc] init];
[self.object1 description];
[self.object2 description];

[self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

[self.object1 description];
[self.object2 description];

self.object1.name = @"lxz";
self.object1.age = 20;

下面是KVO前后的打印信息

// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA

object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

我们发现对象被KVO后,其真正类型变为了NSKVONotifying_KVOObject类,已经不是之前的类了。KVO会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。KVO为了使其更像之前的类,还会将对象的class实例方法重写,使其更像原类。

自定义kvo实现

static void *const zs_KVOObserverAssociatedKey = (void *)&zs_KVOObserverAssociatedKey;
static NSString *zs_KVOClassPrefix = @"zs_KVONotifying_";
@implementation KVOObserverItem

- (instancetype)initWithObserver:(NSObject *)observer
                             key:(NSString *)key
                           block:(zs_KVOObserverBlock)block {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.key = key;
        self.block = block;
    }
    return self;
}

@end


- (void)zs_addObserver:(NSObject *)observer
       keyPath:(NSString *)keyPath
               callback:(zs_KVOObserverBlock)callback {
    
    // 1. 通过Method判断是否有这个key对应的selector,如果没有则Crash。
    SEL originalSetter = NSSelectorFromString(zs_setterForGetter(keyPath));
    Method originalMethod = class_getInstanceMethod(object_getClass(self), originalSetter);
    if (!originalMethod) {
        NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
        NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
        [exception raise];
    }
    
    // 2. 判断当前类是否是KVO子类,如果不是则创建,并设置其isa指针。
    Class kvoClass = object_getClass(self);
    NSString *kvoClassString = NSStringFromClass(kvoClass);
    if (![kvoClassString hasPrefix:zs_KVOClassPrefix]) {
        kvoClass = [self zs_makeKVOClassWithName:kvoClassString];
        object_setClass(self, kvoClass);
    }
    
    // 3. 如果没有实现,则添加Key对应的setter方法。
    if (![self zs_hasMethodWithKey:originalSetter]) {
        class_addMethod(kvoClass, originalSetter, (IMP)zs_kvoSetter, method_getTypeEncoding(originalMethod));
    }
    
    // 4. 将调用对象添加到数组中。
    KVOObserverItem *observerItem = [[KVOObserverItem alloc] initWithObserver:observer key:keyPath block:callback];
    NSMutableArray<KVOObserverItem *> *observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
    if (observers == nil) {
        observers = [NSMutableArray array];
    }
    [observers addObject:observerItem];
    objc_setAssociatedObject(self, zs_KVOObserverAssociatedKey, observers, OBJC_ASSOCIATION_RETAIN);
}

- (void)zs_removeObserver:(NSObject *)observer
          keyPath:(NSString *)keyPath {
    NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
    [observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
        if (mapTable.observer == observer && keyPath == mapTable.key) {
            [observers removeObject:mapTable];
        }
    }];
}

#pragma mark - ----- Private Method Or Funcation ------

static void zs_kvoSetter(id self, SEL selector, id value) {
    // 1. 获取旧值。
    id (*getterMsgSend) (id, SEL) = (void *)objc_msgSend;
    NSString *getterString = zs_getterForSetter(selector);
    SEL getterSelector = NSSelectorFromString(getterString);
    id oldValue = getterMsgSend(self, getterSelector);
    
    // 2. 创建super的结构体,并向super发送属性的消息。
    id (*msgSendSuper) (void *, SEL, id) = (void *)objc_msgSendSuper;
    struct objc_super objcSuper = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    msgSendSuper(&objcSuper, selector, value);
    
    // 3. 遍历调用block。
    NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
    [observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([mapTable.key isEqualToString:getterString] && mapTable.block) {
            mapTable.block(self, NSStringFromSelector(selector), oldValue, value);
        }
    }];
}

- (BOOL)zs_hasMethodWithKey:(SEL)key {
    NSString *setterName = NSStringFromSelector(key);
    unsigned int count;
    Method *methodList = class_copyMethodList(object_getClass(self), &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        if ([methodName isEqualToString:setterName]) {
            return YES;
        }
    }
    return NO;
}

static NSString * zs_getterForSetter(SEL setter) {
    NSString *setterString = NSStringFromSelector(setter);
    if (![setterString hasPrefix:@"set"]) {
        return nil;
    }
    
    NSString *getterString = [setterString substringWithRange:NSMakeRange(4, setterString.length - 5)];
    NSString *firstString = [setterString substringWithRange:NSMakeRange(3, 1)];
    firstString = [firstString lowercaseString];
    getterString = [NSString stringWithFormat:@"%@%@", firstString, getterString];
    return getterString;
}

static NSString * zs_setterForGetter(NSString *getter) {
    NSString *getterString = getter;
    NSString *firstString = [getterString substringToIndex:1];
    firstString = [firstString uppercaseString];
    
    NSString *setterString = [getterString substringFromIndex:1];
    setterString = [NSString stringWithFormat:@"set%@%@:", firstString, setterString];
    return setterString;
}


- (Class)zs_makeKVOClassWithName:(NSString *)name {
    // 1. 判断是否存在KVO类,如果存在则返回。
    NSString *className = [NSString stringWithFormat:@"%@%@", zs_KVOClassPrefix, name];
    Class kvoClass = objc_getClass(className.UTF8String);
    if (kvoClass) {
        return kvoClass;
    }
    
    // 2. 如果不存在,则创建KVO类。
    kvoClass = objc_allocateClassPair(object_getClass(self), className.UTF8String, 0);
    objc_registerClassPair(kvoClass);
    
    // 3. 重写KVO类的class方法,指向自定义的IMP。
    Method method = class_getInstanceMethod(object_getClass(self), @selector(class));
    const char *types = method_getTypeEncoding(method);
    class_addMethod(kvoClass, @selector(class), (IMP)zs_kvoClass, types);
    
    return kvoClass;
}

static Class zs_kvoClass(id self, SEL selector) {
    return class_getSuperclass(object_getClass(self));
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容