美文网首页
KVO的使用与底层探索

KVO的使用与底层探索

作者: 收纳箱 | 来源:发表于2020-04-13 17:16 被阅读0次

    1. 使用

    1.1 自动通知

    // 调用set方法
    [account setName:@"Savings"];
    
    // 使用KVC forKey或forKeyPath
    [account setValue:@"Savings" forKey:@"name"];
    [document setValue:@"Savings" forKeyPath:@"account.name"];
    
    // 使用 mutableArrayValueForKey: 检索关系代理对象
    Transaction *newTransaction = <#Create a new transaction for the account#>;
    NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
    [transactions addObject:newTransaction];
    

    示例

    @interface ViewController ()
    @property (nonatomic, strong) Person *person;
    @property (nonatomic, strong) NSMutableArray<Person *> *people;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 非集合
        self.person = [[Person alloc] init];
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        self.person.name = @"Tom";
        [self.person setValue:@"Jerry" forKey:@"name"];
        [self setValue:@"Tom" forKeyPath:@"person.name"];
        // 集合
        self.people = [NSMutableArray array];
        Person *person0 = [[Person alloc] init];
        person0.name = @"Tom";
        [self.people addObject:person0];
        Person *person1 = [[Person alloc] init];
        person1.name = @"Jerry";
        [self.people addObject:person1];
        NSString *key = @"people";
        [self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
        Person *person2 = [[Person alloc] init];
        person2.name = @"Frank";
        NSMutableArray *people = [self mutableArrayValueForKey:key];
        [people addObject:person2];
        NSLog(@"People: \n%@", self.people);
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"new name: %@", change[NSKeyValueChangeNewKey]);
        } else if ([keyPath isEqualToString:@"people"]) {
            NSLog(@"new array: %@", change[NSKeyValueChangeNewKey]);
            NSArray<Person *> *people = change[NSKeyValueChangeNewKey];
            NSLog(@"new person: %@", people.firstObject.name);
        }
    }
    @end
    // 输出
    new name: Tom
    new name: Jerry
    new name: Tom
    new array: (
        "<Person: 0x60000276cc20>"
    )
    new person: Frank
    People: 
    (
        "Person name: Tom",
        "Person name: Jerry",
        "Person name: Frank"
    )
    

    1.2 手动通知

    手动通知提供了更自由的方式去决定什么时间,什么方式去通知观察者。想要使用手动通知必须实现automaticallyNotifiesObserversForKey: (或者automaticallyNotifiesObserversOf<Key>)方法。在一个类中同时使用自动和手动通知是可行的。对于想要手动通知的属性,可以根据它的keyPath返回NO,而其对于其他位置的keyPath,要返回父类的这个方法。

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        } else {
            return [super automaticallyNotifiesObserversForKey:key];
        }
    }
    // 或者
    + (BOOL)automaticallyNotifiesObserversOfName {
        return NO;
    }
    
    一对一关系
    - (void)setOpeningBalance:(double)theBalance {
         if (theBalance != _openingBalance) {
            [self willChangeValueForKey:@"openingBalance"];
            _openingBalance = theBalance;
            [self didChangeValueForKey:@"openingBalance"];
         }
    }
    

    如果一个操作会导致多个属性改变,需要嵌套通知:

    - (void)setOpeningBalance:(double)theBalance {
         [self willChangeValueForKey:@"openingBalance"];
         [self willChangeValueForKey:@"itemChanged"];
         _openingBalance = theBalance;
         _itemChanged = _itemChanged + 1;
         [self didChangeValueForKey:@"itemChanged"];
         [self didChangeValueForKey:@"openingBalance"];
    }
    
    一对多的关系

    必须注意不仅仅是这个key改变了,还有它改变的类型以及索引。

    - (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
         [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
         // Remove the transaction objects at the specified indexes.
         [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"transactions"];
    }
    

    1.3 键之间的依赖

    在很多种情况下一个属性的值依赖于在其他对象中的属性。如果一个依赖属性的值改变了,这个属性也需要被通知到。

    一对一关系
    @interface Person : NSObject
    @property (nonatomic, strong, nullable) NSString *firstName;
    @property (nonatomic, strong, nullable) NSString *lastName;
    @property (nonatomic, strong, readonly) NSString *fullName;
    @end
    

    可以重写 keyPathsForValuesAffectingValueForKey: 方法。也可以通过实现 keyPathsForValuesAffecting<Key> 方法来达到前面同样的效果,这里的<Key>就是属性名,不过第一个字母要大写。

    @implementation Person
    - (NSString *)fullName {
        return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
    }
    
    + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"fullName"]) {
            NSArray *affectingKeys = @[@"lastName", @"firstName"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    // 或者
    + (NSSet *)keyPathsForValuesAffectingFullName {
        return [NSSet setWithObjects:@"lastName", @"firstName", nil];
    }
    @end
    
    一对多关系

    keyPathsForValuesAffectingValueForKey:方法不能支持一对多关系。

    举个例子,比如你有一个Department对象,和很多个Employee对象。而Employee有一个salary属性。你可能希望Department对象有一个totalSalary的属性,依赖于所有的Employeesalary

    注册Department成为所有Employee的观察者。当Employee被添加或者被移除时进行计算。

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
        if (context == totalSalaryContext) {
            [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
        }
        else
        // deal with other observations and/or invoke super...
    }
     
    - (void)setTotalSalary:(NSNumber *)newTotalSalary {
        if (totalSalary != newTotalSalary) {
            [self willChangeValueForKey:@"totalSalary"];
            _totalSalary = newTotalSalary;
            [self didChangeValueForKey:@"totalSalary"];
        }
    }
     
    - (NSNumber *)totalSalary {
        return _totalSalary;
    }
    

    2. 实现细节

    2.1 isa-swizzling

    KVO的实现用了一种叫 isa-swizzling 的技术。

    当一个对象的一个属性注册了观察者后,被观察对象的isa指针的就指向了一个系统为我们生成的中间类,而不是我们自己创建的类。在这个类中,系统为我们重写了被观察属性的setter方法。

    通过 object_getClass(id obj) 方法可以获得实例对象真实的类(isa指针的指向)。

    @interface Person : NSObject
    @property (nonatomic, strong, nullable) NSString *name;
    @end
    @implementation Person
    @end
      
    @interface ViewController ()
    @property (nonatomic, strong) Person *p1;
    @property (nonatomic, strong) Person *p2;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.p1 = [[Person alloc] init];
        self.p2 = [[Person alloc] init];  
        self.p1.name = @"Tom";
        
        NSLog(@"before kvo --- p2: %s", object_getClassName(self.p2));
        [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        NSLog(@"after  kvo --- p2: %s", object_getClassName(self.p2));
        
        self.p2.name = @"Jerry";
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"new name: %@", change[NSKeyValueChangeNewKey]);
        }
    }
    @end
    // 输出
    before kvo --- p2: Person
    after  kvo --- p2: NSKVONotifying_Person
    new name: Jerry
    

    我们在p2实例对象被键值观察的前后打印其isa指针(实际使用的类)。

    从结果中我们可以看到isa指针指向了一个中间类NSKVONotifying_Person

    苹果的KVO中间类的命名规则是在类名前添加NSKVONotifying_,如果我们的类叫SonKVO之后的中间类为NSKVONotifying_Son

    2.2 IMP

    我们再看一下KVO前后的函数方法的地址是否一样。

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.p1 = [[Person alloc] init];
        self.p2 = [[Person alloc] init];
        self.p1.name = @"Tom";
        
        NSLog(@"before kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
        [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        NSLog(@" after  kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
        
        self.p2.name = @"Jerry";
    }
    // 输出
    before kvo --- p1: 0x10ccee670 p2: 0x10ccee670
    after  kvo --- p1: 0x10ccee670 p2: 0x7fff258e454b
    

    我们看到监听之间两个实例对象的setName:方法的函数地址相同,KVO之后p2实例对象的setName:方法地址变了。

    我们可以查看一下这个方法地址:

    (lldb) image lookup -a 0x7fff258e454b
          Address: Foundation[0x000000000006954b] (Foundation.__TEXT.__text + 422667)
          Summary: Foundation`_NSSetObjectValueAndNotify
    

    这个是Foundation框架中的一个私有方法_NSSetObjectValueAndNotify

    3. 自定义KVO

    下面我们根据KVO的实现细节,仿写一个简化版的KVO。

    NSString *ObserverKey = @"SetterMethodKey";
    // 根据方法名获取Key
    NSString *getKeyForSetter(NSString *setter) {
        NSRange range = NSMakeRange(3, setter.length - 4);
        NSString *key = [setter substringWithRange:range];
        NSString *letter = [[key substringToIndex:1] lowercaseString];
        key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:letter];
        return key;
    }
    // 实现一个setter和通知函数
    void _MySetObjectValueAndNotify(id self, SEL selector, NSString *name) {
        // 1.调用父类的方法
        struct objc_super superClass = {
            self,
            class_getSuperclass([self class])
        };
        objc_msgSendSuper(&superClass, selector, name);
        // 2.通知观察者
        NSObject *observer = objc_getAssociatedObject(self, &ObserverKey);
        NSString *selectorName = NSStringFromSelector(selector);
        NSString *key = getKeyForSetter(selectorName);
        objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{NSKeyValueChangeNewKey: name}, nil);
    }
    
    @implementation Person
    - (void)snx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
        // 1.创建一个子类
        NSString *oldName = NSStringFromClass([self class]);
        NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
        Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
        objc_registerClassPair(customClass);
        // 2.修改修改isa指针
        object_setClass(self, customClass);
        // 3.重写set方法
        NSString *selectorName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
        SEL sel = NSSelectorFromString(selectorName);
        class_addMethod(customClass, sel, (IMP)_MySetObjectValueAndNotify, "v@:@");
        // 4.绑定观察者
        objc_setAssociatedObject(self, &ObserverKey, observer, OBJC_ASSOCIATION_ASSIGN);
    }
    @end
    

    重要

    使用objc_msgSendSuper时,可能编译器会报错:

    Too many arguments to function call, expected 0, have 3

    解决办法:在Build Setting修改Enable Strict Checking of objc_msgSend Calls为No。

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.p1 = [[Person alloc] init];
        self.p2 = [[Person alloc] init];
        self.p1.name = @"Tom";
        
        NSLog(@"before kvo --- p2: %s", object_getClassName(self.p2));
        NSLog(@"before kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
    //    [self.p2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        [self.p2 snx_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
        NSLog(@"after  kvo --- p2: %s", object_getClassName(self.p2));
        NSLog(@"after  kvo --- p1: %p p2: %p", [self.p1 methodForSelector:@selector(setName:)], [self.p2 methodForSelector:@selector(setName:)]);
        
        self.p2.name = @"Jerry";
    }
    // 输出
    before kvo --- p2: Person
    before kvo --- p1: 0x103514460 p2: 0x103514460
    after  kvo --- p2: CustomKVO_Person
    after  kvo --- p1: 0x103514460 p2: 0x103513f90
    new name: Jerry
    

    相关文章

      网友评论

          本文标题:KVO的使用与底层探索

          本文链接:https://www.haomeiwen.com/subject/blngmhtx.html