KVC&KVO

键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,其属性可以通过简洁、统一的消息传递接口通过字符串参数进行寻址。这种间接访问机制补充了实例变量及其关联访问器方法提供的直接访问。
您通常使用访问器方法来访问对象的属性。get访问器(或getter)返回属性的值。集合访问器(或设置器)设置属性的值。在Objective-C中,您还可以直接访问属性的基础实例变量。以上述任何一种方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名称。随着属性列表的增长或变化,访问这些属性的代码也必须增长或变化。相比之下,符合键值编码的对象提供了一个简单的消息传递界面,该界面在其所有属性中都一致。KVC是一个基本概念,是许多其他可可技术的基础,如KVC、可可绑定、核心数据和AppleScript可性。在某些情况下,键值编码也有助于简化代码。

实现机制(摘自苹果文档,下面有干货非纯文档)

valueforkey

valueForKey:的默认实现,给定一个key参数作为输入,执行以下过程,从接收valueForKey:调用的类实例中操作。

  1. 按顺序在实例中搜索第一个访问器方法,名称如get<Key><key>is<Key>_<key>。如果找到,请调用它,然后继续第5步并附上结果。否则,请继续下一步。

  2. 如果没有找到简单的访问器方法,请在实例中搜索名称与模式countOf<Key>objectIn<Key>AtIndex:(对应NSArray类定义的原始方法)和<key>AtIndexes:(对应NSArray方法objectsAtIndexes:)匹配的方法。

    如果找到其中第一个和至少两个中的一个,请创建一个响应所有NSArray方法的集合代理对象并返回该对象。否则,请继续第3步。

    代理对象随后将其收到的任何NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:消息的某种组合,转换为创建它的键值编码兼容对象。如果原始对象还实现了名为get<Key>:range:的可选方法,则代理对象也会在适当时使用该方法。实际上,代理对象与键值编码兼容对象一起工作,允许基础属性的行为就像NSArray一样,即使它不是。

  3. If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).

    如果找到所有三种方法,请创建一个响应所有NSSet方法的集合代理对象并返回该对象。否则,请继续第4步。

    此代理对象随后将其接收的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:消息的某种组合到创建它的对象。实际上,代理对象与键值编码兼容对象一起工作,允许基础属性的行为就像NSSet一样,即使它不是。

  4. 如果没有找到简单的访问器方法或集合访问方法组,并且接收器的类方法accessInstanceVariablesDirectly返回YES,请按此顺序搜索名为_<key>_is<Key><key>is<Key>的实例变量。如果找到,请直接获取实例变量的值,然后转到第5步。否则,请继续第6步。

  5. 如果检索到的属性值是对象指针,只需返回结果。 如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回。
    如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。

  6. 如果所有其他方法都失败了,请调用valueForUndefinedKey:。默认情况下,这会引发异常,但NSObject的子类可能会提供特定于密钥的行为。

setValueForKey

  1. 按顺序查找第一个名为set<Key>:_set<Key>的访问器。如果找到,请使用输入值(或根据需要打开的值)调用它,然后完成。

  2. 如果没有找到简单的访问器,并且类方法accessInstanceVariablesDirectly返回YES,请按顺序查找名称为_<key>_is<Key><key>is<Key>的实例变量。如果找到,请直接使用输入值(或未包装的值)设置变量并完成。

  3. 找不到访问器或实例变量时,调用setValue:forUndefinedKey:。默认情况下,这会引发异常,但NSObject的子类可能会提供特定于密钥的行为。

Array/Set

mutableOrderedSetValueForKey/mutableSetValueForKey:的默认实现:识别与valueForKey相同的简单访问器方法和有序集访问器方法:,并遵循相同的直接实例变量访问策略,但总是返回可变集合代理对象,而不是valueForKey:返回的不可变集合。此外,它还执行以下操作:

  1. 搜索名称如下的方法insertObject:in<Key>AtIndex: and removeObjectFrom<Key>AtIndex: (对应于 NSMutableOrderedSetclass), 而且insert<Key>:atIndexes:remove<Key>AtIndexes:`(对应 insertObjects:atIndexes:removeObjectsAtIndexes:).

如果找到至少一种插入方法和至少一种删除方法,则返回的代理对象将发送以下方法的组合: insertObject:in<Key>AtIndex:, removeObjectFrom<Key>AtIndex:, insert<Key>:atIndexes:, and remove<Key>AtIndexes: 发送给原始收件人的消息mutableOrderedSetValueForKey: 收到“NSMutableOrderedSet”消息时显示消息。

代理对象还使用名称为replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:等名称的方法,当它们存在于原始对象中时。

  1. 如果没有找到可变集方法,搜索名称为set<Key>:的访问器方法。在这种情况下,返回的代理对象每次收到NSMutableOrderedSet消息时都会向原始接收器发送set<Key>:消息。

  2. 如果既没有找到可变集消息也没有找到访问器,并且接收器的accessInstanceVariablesDirectly类方法返回YES,请按照此顺序搜索名称为_<key><key>的实例变量。如果找到此类实例变量,返回的代理对象会将其收到的任何NSMutableOrderedSet消息转发到实例变量的值,该值通常是NSMutableOrderedSet或其子类之一的实例。

  3. 如果所有其他操作都失败,则返回的代理对象将发送
    setValue:forUndefinedKey:发送给原始收件人的消息mutableOrderedSetValueForKey:每当它收到可变集合消息时。
    setValue:forUndefinedKey:的默认实现会引发NSUndefinedKeyException,但对象可能会覆盖此行为。

自定义实现KVC

因为苹果内部闭源所以根据原理猜测他的实现方式


- (BOOL)performSelectorWithMethodName:(NSString *)aSelector value:(id)anArgument {
    SEL func = NSSelectorFromString(aSelector);
    if ([self respondsToSelector:func]) {
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        [self performSelector:func withObject:anArgument];
        _Pragma("clang diagnostic pop")

        return YES;
    }
    return NO;
}



-(void)jl_setValue:(nullable id)value forKey:(NSString *)key {
    
    //key非空判断
    if (!key || key.length == 0) {
        return;
    }
    
    //找到相关方法 set<Kety> _set<Key> setIs<Key>
    //Key要大写
    NSString * Key = key.capitalizedString;
    
    NSString * setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString * _setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString * setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self performSelectorWithMethodName:setKey value:value]) {
        return;
    }
    if ([self performSelectorWithMethodName:_setKey value:value]) {
        return;
    }
    if ([self performSelectorWithMethodName:setIsKey value:value]) {
        return;
    }
    
    //判断当前是否能否直接复制示例变量
    if ([self.class accessInstanceVariablesDirectly]) {
        @throw  [NSException exceptionWithName:@"JLUnknownKeyException" reason:[NSString stringWithFormat:@"***[%@ valueForUndefineKey:]: this class is not key value codeing-comliant for the key name.****",self] userInfo:nil];
        return;
    }
    
    //4. 找相关示例变量进行赋值
    //4.1定一个收集实例变量的可变数组
    NSMutableArray * mArray = [self getIvarListName];
    //_<key> _is<Key> <key> is<Key>
    
    NSString * _Key = [NSString stringWithFormat:@"_%@:",Key];
    NSString * _isKey = [NSString stringWithFormat:@"_is%@:",Key];
    NSString * isKey = [NSString stringWithFormat:@"is%@:",Key];
    
    NSArray * keyarr = @[_Key,_isKey,key,isKey];
    for (NSString * ikey in keyarr) {
        if ([mArray containsObject:ikey]) {
            Ivar ivar = class_getInstanceVariable([self class], ikey.UTF8String);
            object_setIvar(self, ivar, value);
            return;
        }
    }
    
    @throw  [NSException exceptionWithName:@"JLUnknownKeyException" reason:[NSString stringWithFormat:@"***[%@ valueForUndefineKey:]: this class is not key value codeing-comliant for the key name.****",self] userInfo:nil];
}

-(nullable id)jl_valueforKey:(NSString *)key {
    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }
    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    return nil;
}

上面代码是根据kvc文档简单的猜测了他内部实现了原理。但是他还有个进阶用法,其实上面的原理上面也写到了

请参考下列代码

class Person: NSObject {
    var penArr:[String] = []
}


 Person *p = [Person new];
    p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil];
    NSArray *arr = [p valueForKey:@"pens"]; // 动态成员变量
    NSLog(@"pens = %@", arr);
    //NSLog(@"%@",arr[0]);
    NSLog(@"%d",[arr containsObject:@"pen9"]);
    

正常情况下因为没有pens这个属性调用肯定会崩溃,但是如果进行一步骚操作就不会崩了这个操作就是实现对应的


// 个数
- (NSUInteger)countOfPens {
    return [self.penArr count];
}

//// 获取值
- (id) objectInPensAtIndex:(NSUInteger)index {
    return [NSString stringWithFormat:@"pens %lu", index];
}

// 是否包含这个成员对象
- (id)memberOfPens:(id)object {
    return [self.penArr containsObject:object] ? object : nil;
}

// 迭代器
- (id)enumeratorOfPens {
    // objectEnumerator
    return [self.penArr reverseObjectEnumerator];
}

将该key映射到原来的消息体中。这样就实现了kvc的骚操作。
不得不说看文档还是牛逼啊,以后要多看文档可以发现许多牛逼的东西

KVO

当我们对A类添加监听的时候,系统会自动生成一个NSKVONotifying_A的子类,这个类重写了A的class、superclass、deealloc方法和该属性的Set方法,同时A类的对象的isa指针指向了该虚拟子类。当监听属性改变的时候系统调用NSSetobjectValueandNotify,这个方法的执行流程是(willchangeValueforkey->改变父类的值->didchangeValueforkey->observeValueForKey:ofObject:change:context:),如果设置*automaticallyNotifiesObserversForKey:(NSString )key为NO的时候则需要手动触发KVO即手动调用willchangeValueforkey和didchangeValueforkey.

func _NSSetObjectValueAndNotify {
    ...
    willchangeValueforkey
    ...
    "
    objc_msgSendSupper '改变父类的值(猜测这样实现)
    "
    ...
    didchangeValueforkey
    ...
    observeValueForKey:ofObject:change:context: 
}

Q1: 为什么重写系统的class/superclass、deealloc方法

因为该子类为系统自动生成苹果想伪装成并没有这个类 所以重写class/superclass ,但是调用 objc_getClass()这个方法时候依然会暴露,因为这个方法是调用调用对象的isa指针指向。dealloc则是系统还有一些其他的事情处理

自定义KVO

自定义KVO需要遵守KVO的几个基本原理,下面方法只是简单实现了一个kvo,存在context判断以及多个observer都存在的时候处理。这个都需要加判断

1.重写他的set方法所以当调用addObserver的时候需要判断当前类有没有实现set<KeyPath>方法
2.生成NSKVONotifying_XX的派生类
3. 重写父类的class方法,setKeyPath,dealloc方法

#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
    
    if (getter.length <= 0) { return nil;}
    
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}


- (Class)createChildClassWithKeyPath:(NSString *)keyPath{

    NSString * oldName = NSStringFromClass([self class]);
    //动态生成子类
    NSString * newClassName = [NSString stringWithFormat:@"%@%@",kJLKVOPrefix,oldName];
    
    Class newClass = NSClassFromString(newClassName);
    if (newClass) {///判断类是否已经被注册
        return newClass;
    }
    
    /**
     * 如果内存不存在,创建生成
     * 参数一: 父类
     * 参数二: 新类的名字
     * 参数三: 新类的开辟的额外空间
     */
    // 2.1 : 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 注册类
    objc_registerClassPair(newClass);
    
    // 2.3.1 : 添加class : class的指向是父类
    SEL classSel = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char * classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)jl_class, classTypes);
    
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)jl_setter, setterTypes);
    
    return newClass;
}

- (void)jl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    //1.验证是否存在示例方法不让示例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    // 2.动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3. isa的指向 : KVONotifying_Person
    object_setClass(self, newClass);
    // 4: 保存观察者
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJLKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (void)jl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    // 指回给父类
    Class superClass = [self class];
    object_setClass(self, superClass);
}


static void jl_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 既然观察到了,下一步不就是回调 -- 让我们的观察者调用
    // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: 拿到观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJLKVOAssiociateKey));
    
    // 2: 消息发送给观察者
    SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
//    objc_msgSend(observer,observerSEL);
   objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
//    [self observeValueForKeyPath:keyPath ofObject:nil change:@{keyPath:newValue} context:nil];
//    objc_msgSend(self);
}

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

推荐阅读更多精彩内容