美文网首页
—— KVO、KVC

—— KVO、KVC

作者: 大成小栈 | 来源:发表于2021-06-25 14:55 被阅读0次

1. KVO 的原理

对于下面使用KVO的例子,仅仅调用一行代码,便实现了实现 KVO 机制。

//// 对Person类的调用
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person1.age = 21;
    self.person2.age = 22;
}
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    // NSObject * obj = [change objectForKey:NSKeyValueChangeNewKey];
}

//// Person类定义
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end

@implementation Person
- (void)setAge:(int)age{
    _age = age;
}
@end

在修改属性值时,只有能够触发到target类setter方法的动作才会有KVO回调,而其他的则检测不到。苹果的 KVO 机制设计非常巧妙, 通过运行时动态创建子类来实现。

实例addObserver之前,对象与类得关系

当我们对一个对象进行kvo监听的时候,会生成一个NSKVONotifying_前缀的类,然后我们实际的操作是对这个类进行的。某对象在addObserver:之后,这个对象的isa指针已经指向了NSKVONotifying_前缀的类,其父类被设置为Person类。

  • 验证一
    如果在原有工程中,创建NSKVONotifying_Person类,运行代码会报 KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class 错误,因为原有工程中已经存在该类,故无法运行时生成该类。

  • 验证二
    我们可以在addObserver:前后断点打印对象的isa指针,会发现两个实例对应的打印结果不同。

NSLog(@"KVO之前 - %@", object_getClass(self.person));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %@", object_getClass(self.person));

//log
//2021-06-22 20:02:02.101462+0800 TestProj[81369:8704592] KVO之前 - Person
//2021-06-22 20:02:07.003729+0800 TestProj[81369:8704592] KVO之后 - NSKVONotifying_Person
  • 验证三
    在 person 对象调用 addObserver: forKeyPath: options: context: 方法前后添加如下代码,打印结果不同 。
NSLog(@"KVO之前 - %p", [self.person methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %p", [self.person methodForSelector:@selector(setAge:)]);

//log
2021-06-22 20:12:15.245515+0800 TestProj[81577:8714132] KVO之前 - 0x10414be60
2021-06-22 20:12:23.842112+0800 TestProj[81577:8714132] KVO之后 - 0x7fff207bf79f
实例addObserver之后,对象与类得关系

在addObserver:后调用setage方法,会根据对象的isa找到NSKVONotifying_Person,然后在类的方法列表中找到setage。

通过下面方法,查看NSKVONotifying_Person 的内部结构:

- (void)printMethodNamesOfClass:(Class)cls{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[I];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 释放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

// log,在kvo监听下包含了四个方法:
// NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,

可见Apple是不希望暴露NSKVONotifyin_Person,重写了class方法,大概实现如下:

- (Class) class {
     // 得到类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}

重写setName: 的内部实现,其中调用了"_NSSetObjectValueAndNotify()" :

- (void)setName:(NSString *)name {
    _NSSetObjectValueAndNotify()
}

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
    [observer observeValueForKeyPath:@"name"];
}

void _NSSetObjectValueAndNotify() {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

2. 手动触发KVO

因为 KVO 的本质是重写了 set 方法, set 方法内部调用了willChangeValueForKey 和 didChangeValueForKey 方法,直接修改成员变量并不会调用 set 方法。由此可知,KVO 的触发条件一般是修改监听对象属性值,但也可在不修改被监听属性值的情况下触发 KVO 监听回调。

[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];

注:直接修改成员变量不会触发 KVO 监听方法,

3. KVC触发KVO

kvc常见的API有:

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

setValue:forKey:的原理,

setValue:forKey:流程

方法accessInstanceVariablesDirectly:(是否允许访问成员变量),默认返回YES。该方法有个应用场景就是如果你自己写框架,你的一些私有的变量不想被外部通过KVC的方式去修改,就可以重写这个方法,返回 NO 即可!

valueForKey:的原理,

valueForKey:流程

KVC修改属性值,是会触发KVO的,原因是系统自动实现了set方法,并且底层都会调用 willChangeValueForKey和 didChangeValueForKey。

注意:
1.当调用setValue:forKey:时,程序会先通过setter方法,对属性值进行设置;
2.如果没找到setKey:方法,KVC机制会检查accessInstanceVariablesDirectly方法,默认返回YES的,如果重写返回了NO,那么这一步会执行setValue:forUndefinedKey:方法抛出异常。若为YES,KVC机制会搜索该类中是否有名为key的成员变量,若存在,KVC都可以对该成员变量赋值。
3.若该类既没有setKey方法,也没有_key成员变量,KVC机制会搜索_isKey的成员变量;
4.若_isKey成员变量也没有,KVC机制再会继续搜索和is的成员变量给它们赋值;
5.若上述方法或者成员变量都不存在,系统会执行该对象的setValue:forUndefinedKey:方法
,并抛出异常。
即,如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。

因此,触发kvc的set方法才可以触发kvo机制。

// 这个可以完美触发kvo
[_person setValue:@"名字" forKey:@"name"];
// 这个直接给name赋值,所以不能触发kvo
[_person setValue:@"名字" forKey:@"_name"];

4. 手动实现KVO

苹果给的KVO接入也有比较明显的局限,比如无法自定义addObserver:方法、无法传入block、需要处理父类同样监听同一个对象的同一个属性的情况等。我们也是可以自己实现一套KVO逻辑的,首先创建 NSObject 的分类NSObject (KVO):

//// .h
#import <Foundation/Foundation.h>

@class PGObservationInfo;
typedef void(^PGObservingBlock)(PGObservationInfo *observerInfo, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block;
- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end


@interface PGObservationInfo : NSObject

@property (nonatomic, copy) NSString *key;
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) PGObservingBlock block;

- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(PGObservingBlock)block;

@end


//// .m
#import "NSObject+KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

NSString *const kPGKVOClassPrefix = @"PGKVOClassPrefix_";
NSString *const kPGKVOAssociatedObservers = @"PGKVOAssociatedObservers";

#pragma mark - Debug Help Methods
static NSArray *ClassMethodNames(Class c) {
    
    NSMutableArray *array = [NSMutableArray array];
    
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }
    free(methodList);
    
    return array;
}


static void PrintDescription(NSString *name, id obj)
{
    NSString *str = [NSString stringWithFormat:
                     @"%@: %@\n\tNSObject class %s\n\tRuntime class %s\n\timplements methods <%@>\n\n",
                     name,
                     obj,
                     class_getName([obj class]),
                     class_getName(object_getClass(obj)),
                     [ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
    printf("%s\n", [str UTF8String]);
}


#pragma mark - Helpers
static NSString * getterForSetter(NSString *setter)
{
    if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
        return nil;
    }
    
    // remove 'set' at the begining and ':' at the end
    NSRange range = NSMakeRange(3, setter.length - 4);
    NSString *key = [setter substringWithRange:range];
    
    // lower case the first letter
    NSString *firstLetter = [[key substringToIndex:1] lowercaseString];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1)
                                       withString:firstLetter];
    
    return key;
}


static NSString * setterForGetter(NSString *getter)
{
    if (getter.length <= 0) {
        return nil;
    }
    
    // upper case the first letter
    NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
    NSString *remainingLetters = [getter substringFromIndex:1];
    
    // add 'set' at the begining and ':' at the end
    NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
    
    return setter;
}


#pragma mark - Overridden Methods
static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    id oldValue = [self valueForKey:getterName];
    
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    for (PGObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

static Class kvo_class(id self, SEL _cmd)
{
    return class_getSuperclass(object_getClass(self));
}


#pragma mark - KVO Category
@implementation NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block {
    
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key];
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
        return;
    }
    
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    
    // if not an KVO class yet
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    // add our kvo setter if this class (not superclasses) doesn't implement the setter?
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
    
    PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}


- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key
{
    NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    
    PGObservationInfo *infoToRemove;
    for (PGObservationInfo* info in observers) {
        if (info.observer == observer && [info.key isEqual:key]) {
            infoToRemove = info;
            break;
        }
    }
    
    [observers removeObject:infoToRemove];
}


- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}


- (BOOL)hasSelector:(SEL)selector
{
    Class clazz = object_getClass(self);
    unsigned int methodCount = 0;
    Method* methodList = class_copyMethodList(clazz, &methodCount);
    for (unsigned int i = 0; i < methodCount; i++) {
        SEL thisSelector = method_getName(methodList[i]);
        if (thisSelector == selector) {
            free(methodList);
            return YES;
        }
    }
    
    free(methodList);
    return NO;
}

@end


@implementation PGObservationInfo

- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(PGObservingBlock)block {
    
    self = [super init];
    if (self) {
        _observer = observer;
        _key = key;
        _block = block;
    }
    return self;
}

@end

其中复杂的Observer独立成一个类,自行创建工程,试一试吧!😜

参考文章:
http://www.cocoachina.com/articles/25407
https://tech.glowing.com/cn/implement-kvo/
https://www.jianshu.com/p/79b06fabb459
感谢!

相关文章

网友评论

      本文标题:—— KVO、KVC

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