iOS 底层原理探索 之 魔法师KVC

867 阅读9分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象

以上内容的总结专栏


细枝末节整理


KVC是什么

键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制来提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁、统一的消息传递接口进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

您通常使用访问器方法来访问对象的属性。get 访问器(或 getter)返回属性的值。set 访问器(或 setter)设置属性的值。在 Objective-C 中,您还可以直接访问属性的底层实例变量。以任何这些方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名称。随着属性列表的增长或变化,访问这些属性的代码也必须如此。相比之下,符合键值编码的对象提供了一个简单的消息传递接口,该接口在其所有属性中保持一致。

键值编码是一个基本概念,它是许多其他 Cocoa 技术的基础,例如 key-value observing, Cocoa bindings, Core Data, and AppleScript-ability。在某些情况下,键值编码还有助于简化您的代码。

KVC常用的两个API如下:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

点方法进去可以看到:

image.png

它是在 Foundation 框架下,关于 NSObject 的一个(NSKeyValueCoding)扩展。通过简单查看,这里的api是非常之多的,那么,我们要想详细的了解其内部的实现原理,就要去查阅下苹果的官方文档中关于KVC的解释说明

对于KVC的概念,以及其使用苹果的文档有较为详细的使用说明。本文就不过多的搬运,上面的链接供大家自行阅读学习。

KVC赋值取值的过程

NSKeyValueCoding协议 的默认实现NSObject使用一组明确定义的规则将基于键的访问器调用映射到对象的底层属性。这些协议方法使用一个关键参数来搜索它们自己的对象实例以查找访问器、实例变量和遵循某些命名约定的相关方法。尽管您很少修改此默认搜索,但了解它的工作原理会很有帮助,无论是跟踪键值编码对象的行为,还是使您自己的对象合规。

基本 Getter 的搜索模式

的默认实现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. 如果没有找到简单的访问方法或阵列访问方法组,寻找一个三重的方法命名countOf<Key>enumeratorOf<Key>memberOf<Key>:(对应于由所定义的原始的方法NSSet类)。

    如果找到所有三个方法,则创建一个响应所有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可能会提供特定于键的行为。

基本 Setter 的搜索模式

的默认实现setValue:forKey:,给定的keyvalue参数作为输入,尝试设置命名属性keyvalue(或,对于非对象属性,的展开的版本 value,如在代表非对象值的物体内接收到呼叫,使用以下程序:

  1. 按顺序 查找名为set<Key>:or的第一个访问器_set<Key>。如果找到,则使用输入值(或根据需要展开的值)调用它并完成。
  2. 如果没有找到简单的访问,如果类方法accessInstanceVariablesDirectly返回YES,寻找一个实例变量与名称类似_<key>_is<Key><key>,或者is<Key>,按照这个顺序。如果找到,直接使用输入值(或解包值)设置变量并完成。
  3. 找不到访问器或实例变量后,调用setValue:forUndefinedKey:. 默认情况下,这会引发异常,但 的子类NSObject可能会提供特定于键的行为。

苹果文档缺失的内容

按照苹果文档,在其第一步查找的名字为 set<Key>: 或者 _set<Key> ;但是第二部的话,却是在accessInstanceVariablesDirectly返回YES后,寻找了 _<key>_is<Key><key>,或者is<Key>。显然,我会对第一步骤有所怀疑,我们探索下,如果我门实现了setIsKsy是否会查找到。 验证: 会走到setIsKey方法哦!这也是苹果文档缺失的内容 image.png

然而_setIsKey 方法并不会和猜想的那样会去查找的哦

image.png

这是一个探索的过程,需要耐心。

setValue forKey流程图

sm_setValue.001.jpeg

整理了流程图,我们就可以自定义一下实现过程:

- (void)sm_setValue:(id)value forKey:(NSString *)key {
    
    if (key == nil || key.length == 0) {
        
        return ;
    }
    
    NSString *Key = key.capitalizedString;
    
    //1、 set<Key>: / _set<Key>
    //2、 accessInstanceVariablesDirectly and _<key>,_is<Key>,<key> is<Key>
    //3、 exception
    
    SEL setKey = NSSelectorFromString([NSString stringWithFormat:@"set%@", Key]);
    SEL _setKey = NSSelectorFromString([NSString stringWithFormat:@"_set%@", Key]);
    SEL _setIsKey = NSSelectorFromString([NSString stringWithFormat:@"_setIs%@", Key]);
    
    if ([self performSelector:setKey withObject:value]) {
        
        NSLog(@" setKey ");
        return;
    }
    
    if ([self performSelector:_setKey withObject:value]) {
        
        NSLog(@" _setKey ");
        return;
    }
    
    if ([self performSelector:_setIsKey withObject:value]) {
        
        NSLog(@" _setKey ");
        return;
    }
    
    if ([[self class] accessInstanceVariablesDirectly]) {
        
   
    unsigned int outCount = 0;
    
    ///获取实例变量列表
    Ivar *ivars = class_copyIvarList([self class], &outCount);
    NSMutableArray *is = [NSMutableArray arrayWithCapacity:2];
    
    for (int i = 0; i < outCount; i++) {
        
        Ivar ivar = ivars[i];
        const char *pName = ivar_getName(ivar);
        NSString *name = [NSString stringWithCString:pName encoding:NSUTF8StringEncoding];
        
        [is addObject:name];
    }
    free(ivars);
    
    NSString *_key = [NSString stringWithFormat:@"_%@", key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@", Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@", Key];
    
    if ([is containsObject:_key]) {
        
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        
        object_setIvar(self, ivar, value);
        return ;
    }else if ([is containsObject:_isKey]) {
        
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        
        object_setIvar(self, ivar, value);
        return;
    }else if ([is containsObject:key]) {
        
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        
        object_setIvar(self, ivar, value);
        return;
    }else if ([is containsObject:isKey]) {
        
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        
        object_setIvar(self, ivar, value);
        return;
    }
        
    }
    
    [self setValue:value forUndefinedKey:key];
}

valueForKey 流程图

myNoteBase_副本2.001.jpeg

整理了流程图,我们就可以自定义一下实现过程:

    if (key == nil || key.length == 0) {
        
        return [NSNull null];
    }
    
    NSString *Key = key.capitalizedString;
    
    SEL getKeyM = NSSelectorFromString([NSString stringWithFormat:@"get%@", Key]);
    SEL keyM = NSSelectorFromString(key);
    SEL isKeyM = NSSelectorFromString([NSString stringWithFormat:@"is%@", key]);
    SEL _keyM = NSSelectorFromString([NSString stringWithFormat:@"_%@", key]);
    ///  1、 搜索实例的方法 getKey \ key \ isKey \ _key
    if ([self respondsToSelector:getKeyM]) {
        
        return [self performSelector:getKeyM];
    }
    if ([self respondsToSelector:keyM]) {
        
        return [self performSelector:keyM];
    }
    if ([self respondsToSelector:isKeyM]) {
        
        return [self performSelector:isKeyM];
    }
    if ([self respondsToSelector:_keyM]) {
        
        return [self performSelector:_keyM];
    }
    
    
    
    /// 4、 查看接收器的类方法 accessInstanceVariablesDirectly 是否返回了YES
    /// YES的话 搜索实例变量 _key _isKey key 或者isKey 按照这个顺序。如果找到了,直接获取实例变量的值
    if ([[self class] accessInstanceVariablesDirectly]) {
        
        unsigned int outCount = 0;
        
        ///获取实例变量列表
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        NSMutableArray *is = [[NSMutableArray alloc] init];
        
        for (int i = 0; i < outCount; i++) {
            
            Ivar ivar = ivars[i];
            const char *pName = ivar_getName(ivar);
            NSString *name = [NSString stringWithUTF8String:pName];
            
            [is addObject:name];
        }
        
        NSString *_key = [NSString stringWithFormat:@"_%@", key];
        NSString *_isKey = [NSString stringWithFormat:@"_is%@", Key];
        NSString *isKey = [NSString stringWithFormat:@"is%@", Key];
        
        if ([is containsObject:_key]) {
            
            Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
            
            return object_getIvar( self, ivar ) ;
        }else if ([is containsObject:_isKey]) {
            
            Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
            
            return object_getIvar( self, ivar ) ;
        }else if ([is containsObject:key]) {
            
            Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
            
            return object_getIvar( self, ivar ) ;
        }else if ([is containsObject:isKey]) {
            
            Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
            
            return object_getIvar( self, ivar ) ;
        }
        
        
        
    }
        
        /// 6、 调用 valueForUndefinedKey:
    return [self performSelector:@selector(valueForUndefinedKey:) withObject:key];

总结

本片内容较为简单,因为不想将苹果的文档做翻译搬运,只是做了对于 setValue:(id)value forKey:(NSString *)keyvalueForKey:(NSString *)key 方法中对于属性 简单值 的分析, 另外 对于 这些是具有自己属性的可变对象集合对象 等本文没有涉及到,请移步 Key-Value Coding Programming Guide 苹果官方有详细的文档介绍。