美文网首页源码解析
GNUstep KVC/KVO探索(一):KVC的内部实现

GNUstep KVC/KVO探索(一):KVC的内部实现

作者: 哦呵呵y | 来源:发表于2018-10-18 16:00 被阅读52次

    GNUstep KVC/KVO探索(一):KVC的内部实现
    GNUstep KVC/KVO探索(二):KVO的内部实现

    KVC全称是Key Value Coding,定义在NSKeyValueCoding.h文件中,是一个非正式协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。

    在NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKey:和setter方法setValue:forKey:,以及其衍生的keyPath方法,这两个方法各个类通用的。并且由KVC提供默认的实现。

    GNUstep和Cocoa都是基于OpenStep演变而来,所以底层实现有很多相似之处,可以通过GNUstep源码来分析KVC的底层实现。

    一、Set方法

    - (void) setValue: (id)anObject forKey: (NSString*)aKey
    {
      unsigned  size = [aKey length] * 8;
      char      key[size + 1];
    
      ....
    
      [aKey getCString: key
         maxLength: size + 1
          encoding: NSUTF8StringEncoding];
      size = strlen(key);
      SetValueForKey(self, anObject, key, size);
    }
    
    1. 将传入的key转成字符数组,然后调用SetValueForKey(),主要实现内容在SetValueForKey()
    2. 中间省略的部分为老版本的兼容设置
    static void
    SetValueForKey(NSObject *self, id anObject, const char *key, unsigned size)
    {
      SEL       sel = 0;
      const char    *type = 0;
      int       off = 0;
    
      if (size > 0)
        {
          const char    *name;
          char      buf[size + 6];
          char      lo;
          char      hi;
    
          strncpy(buf, "_set", 4);
          strncpy(&buf[4], key, size);
          lo = buf[4];
          hi = islower(lo) ? toupper(lo) : lo;
          buf[4] = hi;
          buf[size + 4] = ':';
          buf[size + 5] = '\0';
    
          name = &buf[1];   // setKey:
          type = NULL;
          sel = sel_getUid(name);
          if (sel == 0 || [self respondsToSelector: sel] == NO)
        {
          name = buf;   // _setKey:
          sel = sel_getUid(name);
          if (sel == 0 || [self respondsToSelector: sel] == NO)
            {
              sel = 0;
              if ([[self class] accessInstanceVariablesDirectly] == YES)
            {
              buf[size + 4] = '\0';
              buf[3] = '_';
              buf[4] = lo;
              name = &buf[3];   // _key
              if (GSObjCFindVariable(self, name, &type, &size, &off) == NO)
                {
                  buf[4] = hi;
                  buf[3] = 's';
                  buf[2] = 'i';
                  buf[1] = '_';
                  name = &buf[1];   // _isKey
                  if (GSObjCFindVariable(self,
                name, &type, &size, &off) == NO)
                {
                  buf[4] = lo;
                  name = &buf[4];   // key
                  if (GSObjCFindVariable(self,
                    name, &type, &size, &off) == NO)
                    {
                      buf[4] = hi;
                      buf[3] = 's';
                      buf[2] = 'i';
                      name = &buf[2];   // isKey
                      GSObjCFindVariable(self,
                    name, &type, &size, &off);
                    }
                }
                }
            }
            }
          else
            {
              GSOnceFLog        (@"Key-value access using _setKey: is deprecated:");
            }
        }
        }
      GSObjCSetVal(self, key, anObject, sel, type, size, off);
    }
    

    此方法的实现很简单,主要是获取key相关的方法或者变量,然后调用GSObjCSetVal()进行实际赋值操作

    1. 方法中利用传进来的字符数组,进行拼接,依次查询setKey--->_setKey--->_key--->_isKey--->key--->isKey
    2. accessInstanceVariablesDirectly通过此参数判断当关联方法找不到时,是否可以直接操作实例变量,默认实现为YES
    3. 最后GSObjCSetVal()传入查询到的方法或者变量,进行实际赋值操作
    void
    GSObjCSetVal(NSObject *self, const char *key, id val, SEL sel,
      const char *type, unsigned size, int offset)
    {
      static NSNull     *null = nil;
      NSMethodSignature *sig = nil;
    
      if (null == nil)
        {
          null = [NSNull new];
        }
      if (sel != 0)
        {
          sig = [self methodSignatureForSelector: sel];
          if ([sig numberOfArguments] != 3)
        {
          [NSException raise: NSInvalidArgumentException
                  format: @"key-value set method has wrong number of args"];
        }
          type = [sig getArgumentTypeAtIndex: 2];
        }
      if (type == NULL)
        {
          [self setValue: val forUndefinedKey:
        [NSString stringWithUTF8String: key]];
        }
      else if ((val == nil || val == null) && *type != _C_ID && *type != _C_CLASS)
        {
          [self setNilValueForKey: [NSString stringWithUTF8String: key]];
        }
      else
        {
          switch (*type)
        {
          case _C_ID:
          case _C_CLASS:
            {
              id    v = val;
    
              if (sel == 0)
            {
              id *ptr = (id *)((char *)self + offset);
    
              ASSIGN(*ptr, v);
            }
              else
            {
              void  (*imp)(id, SEL, id) =
                (void (*)(id, SEL, id))[self methodForSelector: sel];
    
              (*imp)(self, sel, val);
            }
            }
            break;
    
          case _C_CHR:
            {
              char  v = [val charValue];
    
              if (sel == 0)
            {
              char *ptr = (char *)((char *)self + offset);
    
              *ptr = v;
            }
              else
            {
              void  (*imp)(id, SEL, char) =
                (void (*)(id, SEL, char))[self methodForSelector: sel];
    
              (*imp)(self, sel, v);
            }
            }
            break;
    
          case _C_UCHR:
          case _C_UCHR:
          case _C_SHT:
          case _C_USHT:
          case _C_INT:
    
    ...
    
          default:
                [self setValue: val forUndefinedKey:
              [NSString stringWithUTF8String: key]];
        }
        }
    }
    

    此方法为KVC中实际赋值方法,主要做了一下几件事:

    1. 判断方法是否存在,如果存在,判断方法类型是否正确,set方法的参数应该有三个self _cmd value, 不正确就抛出异常, 如果方法正确,则通过getArgumentTypeAtIndex获取到方法中value参数的类型
    2. 此时如果方法存在,则获取到方法中参数的类型,如果方法不存在,则会使用实参传递进来的实例变量的类型
    3. 判断类型是否为空,如果为空,则代表没有key相关的信息,调用setValue:forUndefinedKey:方法,方法默认实现为抛出异常,子类实现此方法可避免崩溃
    4. 然后判断要赋值的value如果为nil/null,并且参数类型不是指针类型和类,则调用setNilValueForKey:,默认实现为抛出异常
    5. 此时已经获取了所有信息,判断参数类型,在根据获取的相关联信息,是方法还是实例变量进行不同操作,
      如果是实例变量,则将参数转成相匹配类型,然后利用指针赋值
      如果是方法,则根据methodForSelector:获取相应类型的IMP指针进行调用。
    6. 最后如果类型匹配不到,则调用setValue:forUndefinedKey:方法抛出异常

    二、get方法

    - (id) valueForKey: (NSString*)aKey
    {
      unsigned  size = [aKey length] * 8;
      char      key[size + 1];
    
      [aKey getCString: key
         maxLength: size + 1
          encoding: NSUTF8StringEncoding];
      size = strlen(key);
      return ValueForKey(self, key, size);
    }
    
    static id ValueForKey(NSObject *self, const char *key, unsigned size)
    {
      SEL       sel = 0;
      int       off = 0;
      const char    *type = NULL;
    
      if (size > 0)
        {
          const char    *name;
          char      buf[size + 5];
          char      lo;
          char      hi;
    
          strncpy(buf, "_get", 4);
          strncpy(&buf[4], key, size);
          buf[size + 4] = '\0';
          lo = buf[4];
          hi = islower(lo) ? toupper(lo) : lo;
          buf[4] = hi;
    
          name = &buf[1];   // getKey
          sel = sel_getUid(name);
          if (sel == 0 || [self respondsToSelector: sel] == NO)
        {
          buf[4] = lo;
          name = &buf[4];   // key
          sel = sel_getUid(name);
          if (sel == 0 || [self respondsToSelector: sel] == NO)
            {
                  buf[4] = hi;
                  buf[3] = 's';
                  buf[2] = 'i';
                  name = &buf[2];   // isKey
                  sel = sel_getUid(name);
                  if (sel == 0 || [self respondsToSelector: sel] == NO)
                    {
                      sel = 0;
                    }
            }
        }
    
          if (sel == 0 && [[self class] accessInstanceVariablesDirectly] == YES)
        {
          buf[4] = hi;
          name = buf;   // _getKey
          sel = sel_getUid(name);
          if (sel == 0 || [self respondsToSelector: sel] == NO)
            {
              buf[4] = lo;
              buf[3] = '_';
              name = &buf[3];   // _key
              sel = sel_getUid(name);
              if (sel == 0 || [self respondsToSelector: sel] == NO)
            {
              sel = 0;
            }
            }
          if (sel == 0)
            {
              if (GSObjCFindVariable(self, name, &type, &size, &off) == NO)
            {
                      buf[4] = hi;
                      buf[3] = 's';
                      buf[2] = 'i';
                      buf[1] = '_';
                      name = &buf[1];   // _isKey
              if (!GSObjCFindVariable(self, name, &type, &size, &off))
                        {
                           buf[4] = lo;
                           name = &buf[4];      // key
                   if (!GSObjCFindVariable(self, name, &type, &size, &off))
                             {
                                buf[4] = hi;
                                buf[3] = 's';
                                buf[2] = 'i';
                                name = &buf[2]; // isKey
                                GSObjCFindVariable(self, name, &type, &size, &off);
                             }
                        }
            }
            }
        }
        }
      return GSObjCGetVal(self, key, sel, type, size, off);
    }
    
    id
    GSObjCGetVal(NSObject *self, const char *key, SEL sel,
               const char *type, unsigned size, int offset)
    {
      NSMethodSignature *sig = nil;
    
      if (sel != 0)
        {
          sig = [self methodSignatureForSelector: sel];
          if ([sig numberOfArguments] != 2)
        {
          [NSException raise: NSInvalidArgumentException
                  format: @"key-value get method has wrong number of args"];
        }
          type = [sig methodReturnType];
        }
      if (type == NULL)
        {
          return [self valueForUndefinedKey: [NSString stringWithUTF8String: key]];
        }
      else
        {
          id    val = nil;
    
          switch (*type)
        {
          case _C_ID:
          case _C_CLASS:
            {
              id    v;
    
              if (sel == 0)
            {
              v = *(id *)((char *)self + offset);
            }
              else
            {
              id    (*imp)(id, SEL) =
                (id (*)(id, SEL))[self methodForSelector: sel];
    
              v = (*imp)(self, sel);
            }
              val = v;
            }
            break;
          case _C_UCHR:
          case _C_UCHR:
          case _C_SHT:
          case _C_USHT:
          case _C_INT:
    ....
    
          default:
    #ifdef __GNUSTEP_RUNTIME__
            {
              Class     cls;
              struct objc_slot  *type_slot;
              SEL       typed;
              struct objc_slot  *slot;
    
              cls = [self class];
              type_slot = objc_get_slot(cls, @selector(retain));
              typed = GSSelectorFromNameAndTypes(sel_getName(sel), NULL);
              slot = objc_get_slot(cls, typed);
              if (strcmp(slot->types, type_slot->types) == 0)
            {
              return slot->method(self, typed);
            }
            }
    #endif
            val = [self valueForUndefinedKey:
              [NSString stringWithUTF8String: key]];
        }
          return val;
        }
    }
    
    
    1. get方法与set方法类似,都是根据key一次查询相关信息,查询顺序为getKey--->key--->isKey--->_getKey--->_key(方法)--->_key(实例变量)--->_isKey--->key--->isKey, 前5个查询为方法,后4个为实例变量。
    2. 同样根据accessInstanceVariablesDirectly判断是否直接操作实例变量,其中包含_getKey _key方法
    3. 然后根据获取到的内容调用GSObjCGetVal()获取值
    4. GSObjCGetVal()GSObjCSetVal()逻辑相似,不过用来判断的是方法返回值的类型进行判断。同样,如果类型为空,则调用valueForUndefinedKey:方法抛出异常。
    5. 然后根据类型、方法/实例变量进行取值,如果是方法,则获取IMP指针调用,如果只实例变量,则直接根据指针取值。
    6. 最后如果类型匹配不到,则调用valueForUndefinedKey:方法抛出异常。

    三、KeyPath 相关

    KVC中可以使用点语法,进行深层次的赋值、取值。

    - (void) setValue: (id)anObject forKeyPath: (NSString*)aKey
    {
      NSRange       r = [aKey rangeOfString: @"." options: NSLiteralSearch];
    #ifdef WANT_DEPRECATED_KVC_COMPAT
      IMP           o = [self methodForSelector: @selector(takeValue:forKeyPath:)];
    
      setupCompat();
      if (o != takePath && o != takePathKVO)
        {
          (*o)(self, @selector(takeValue:forKeyPath:), anObject, aKey);
          return;
        }
    #endif
    
      if (r.length == 0)
        {
          [self setValue: anObject forKey: aKey];
        }
      else
        {
          NSString  *key = [aKey substringToIndex: r.location];
          NSString  *path = [aKey substringFromIndex: NSMaxRange(r)];
          
          [[self valueForKey: key] setValue: anObject forKeyPath: path];
        }
    }
    
    - (id) valueForKeyPath: (NSString*)aKey
    {
      NSRange       r = [aKey rangeOfString: @"." options: NSLiteralSearch];
    
      if (r.length == 0)
        {
          return [self valueForKey: aKey];
        }
      else
        {
          NSString  *key = [aKey substringToIndex: r.location];
          NSString  *path = [aKey substringFromIndex: NSMaxRange(r)];
    
          return [[self valueForKey: key] valueForKeyPath: path];
        }
    }
    

    两个方法类似,都是利用递归逐级调用 valueForKey: 以及 setValue:forKey:


    最后

    1. KVC 就是利用字符串,来查询到和对象相关联的方法、实例变量,来达到间接访问属性方法或成员变量。
    2. 由于KVC在访问属性方法或成员变量前加了一层查询机制,所以效率没有直接访问属性方法或成员变量高。
    3. 由于KVC的使用是通过字符串, 所以极易出错,推荐借鉴 RAC中的 @keyPath宏, RAC宏分析8:@keypath
    4. 由上面源码得知,如果key相关联信息找不到,获取传入参数类型不匹配都会导致异常,所以使用时,最后要重写相应的方法
    5. 容器类包含了很多便捷的keyPath,
      @count
      @avg
      @max
      @min
      @sum
      @distinctUnionOfArrays
      @distinctUnionOfObjects
      @distinctUnionOfSets
      @unionOfArrays
      @unionOfObjects
      @unionOfSets
      以上keyPath都是通过容器里重写valueForKeyPath:实现,而且还可以通过数组,一次性返回内部对象的某个属性集合。
      由于这些方法都是通过KVC实现,所以效率比较低,而且由于返回值为对象,所以还进行了不必要的封装

    相关文章

      网友评论

        本文标题:GNUstep KVC/KVO探索(一):KVC的内部实现

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