KVC

KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject)
KVC使用
KVC在iOS开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,KVC也是许多iOS开发黑魔法的基础。
下面列举iOS开发中KVC的使用场景.

  • 动态地取值和设值
    利用KVC动态的取值和设值是最基本的用途了。
  • 用KVC来访问和修改私有变量
    对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。
  • Model和字典转换
    这是KVC强大作用的又一次体现,KVC和Objc的runtime组合可以很容易的实现Model和字典的转换。
  • 修改一些控件的内部属性
    这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。
    而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText了。
  • 操作集合
    Apple对KVC的valueForKey:方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法。所以可以用KVC很方便地操作集合。
    通过调用valueForKey:给容器中每一个对象发送操作消息,并且结果会被保存在一个新的容器中返回,这样我们能很方便地利用一个容器对象创建另一个容器对象另外,valueForKeyPath:还能实现多个消息的传递,厉害了
NSArray*array=[NSArrayarrayWithObject:@"10.11",
@"20.22",nil];
NSArray*resultArray=[array valueForKeyPath:@"doubleValue.intValue"];
NSLog(@"%@",resultArray);
//打印结果
(
10,
20
)
  • 用KVC实现高阶消息传递

当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *arrStr = @[@"English", @"franch", @"chinese"];
        NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
        for (NSString *str in arrCapStr) {
            NSLog(@"%@", str);
        }
       
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.integerValue);
        }
    }
    return 0;
}

ios 项目中那些地方用到了kvo ios kvc使用场景_派生类


对于keyPath是搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去

setValue 设值顺序

查找setKey 方法,有则调用,没有则查找看有没有_setKey方法,有则调用。没有则查看 accessInstanceVariablesDirectly 这个类方法的返回值,是yes则按照_key 、_isKey 、key 、iskey 顺序查找成员变量。如果没有找到则抛出异常 NSUnknownKeyException 。如果accessInstanceVariablesDirectly返回值是 NO,则程序抛出异常
然后valueForKey是一样的

KVO 原理

网上有许多总结的KVO的isa-swizzling技术的大体实现流程。让我们总结一下

  • 当类实例被KVO后,系统会替换实例的isa指针内容。让其指向NSKVONotifing_XX类型的新类。
  • 在NSKVONotifing_XX类中,会:重写KVO属性的set方法,支持KVO。重写class方法,来伪装自己仍然是XX类。添加_isKVOA方法,来说明自己是一个KVO类。重写dealloc方法,让实例下析构时,好做一些检查和清理工作
  • 为了让用户在KVO isa-swizzling后,仍然能够调用原始XX类中的方法,系统还会将NSKVONotifing_XX类设置为原始XX类的子类。
  • 当移除KVO后,系统会将isa指针中的内容复原。

原理中提到新建了一个NSKVONotifing_XX来。。。
这样在项目开发中就有了一种新的方法:Method Swizzling
交换方法的实现一些需求

Method Swizzling类簇
 ??
 NSArray __NSArrayI
 NSMutableArra __NSArrayM
 NSDictionary __NSDictionaryI
 NSMutableDictionary __NSDictionaryM
 ? 封装的Method Swizzling ????????????????????eJRSwizzle

源码分析

static GSKVOReplacement * replacementForClass(Class c) {
    GSKVOReplacement *r;

    setup();
    [kvoLock lock];
    r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
    if (r == nil)
      {
        r = [[GSKVOReplacement alloc] initWithClass: c];
        NSMapInsert(classTable, (void*)c, (void*)r);
      }
    [kvoLock unlock];
    return r;
}

这个函数呢就生成了新的派生类GSKVOReplacement中的一个名为replacement的Class对象
在这个函数中,首先执行了静态内联函数setup()。在setup函数中,初始化了classTable等相关的表,并且初始化了静态变量baseClass,这个baseClass是一个GSKVOBase类对象
在replacementForClass()函数中通过传入的原Class比如MyObject,在classTable中找寻派生类比如KVO_MyObject。如果没找到,通过GSKVOReplacement的initWithClass:方法新建一个并且插入到classTable中。
那么看看initWithClass:又是如何实现的

original = aClass;

 /*
  * Create subclass of the original, and override some methods
  * with implementations from our abstract base class.
  */
 superName = NSStringFromClass(original);
 name = [@"GSKVO" stringByAppendingString: superName];
 template = GSObjCMakeClass(name, superName, nil);
 GSObjCAddClasses([NSArray arrayWithObject: template]);
 replacement = NSClassFromString(name);
 GSObjCAddClassBehavior(replacement, baseClass);

 /* Create the set of setter methods overridden.
  */
 keys = [NSMutableSet new];

 return self;

根据代码大致流程如下:

  1. 通过原类拼接派生类的子类名
  2. 通过GSObjCAddClasses创建新类,在该函数中,通过superName反射创建superClass,也就是MyObject的class。通过name和objc_allocateClassPair函数创建一个新的class,也就是派生类。将生成的class对象包装在NSValue中返回给template
  3. 通过GSObjCAddClasses将template里的派生类对象注册好
  4. 注册好了以后就可以通过name 反射得到派生类replacement
  5. 通过GSObjCAddClassBehavior为replacement添加相应的方法,这个函数的两个参数为派生类和baseClass,而这个baseClass呢,就是
    GSKVOBase类,这个类大概就是所有派生类的基类吧
GSObjCAddClassBehavior(Class receiver, Class behavior) {
        unsigned int	count;
     Method	*methods;
     Class behavior_super_class = class_getSuperclass(behavior);
     
     	...
    /* 一些不是很关键的代码 */
    
     /* Add instance methods */
     methods = class_copyMethodList(behavior, &count);
     BDBGPrintf("  instance methods from %s %u\n", class_getName(behavior), count);
     if (methods == NULL)
       {
         BDBGPrintf("    none.\n");
       }
     else
       {
         GSObjCAddMethods (receiver, methods, NO);
         free(methods);
       }
    
     /* Add class methods */
     methods = class_copyMethodList(object_getClass(behavior), &count);
     BDBGPrintf("  class methods from %s %u\n", class_getName(behavior), count);
     if (methods == NULL)
       {
         BDBGPrintf("    none.\n");
       }
     else
       {
         GSObjCAddMethods (object_getClass(receiver), methods, NO);
         free(methods);
       }
    
     /* Add behavior's superclass, if not already there. */
     if (!GSObjCIsKindOf(receiver, behavior_super_class))
       {
         GSObjCAddClassBehavior (receiver, behavior_super_class);
       }
     GSFlushMethodCacheForClass (receiver);
}

主要意思呢,就是把behavior中的方法列表copy一份到receiver中,根据上面的函数调用,我们知道behavior就是GSKVOBase类,而receiver就是派生类,也就是obj的isa最后指向的对象
那么这个GSKVOBase又是什么呢

- (Class) class
{
  return class_getSuperclass(object_getClass(self));
}

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
  Class		c = [self class];
  void		(*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

首先重写了自己的class方法,返回的自己的superClass,那么就相当于派生类的class方法返回的就是MyObject类,所以就是为什么你使用了KVO,打印的类还是原类
第二个方法是通过automaticallyNotifiesObserversForKey判断该key是否被kvo了,如果是就插入[self willChangeValueForKey: aKey]和self didChangeValueForKey: aKey];方法,如果没有就直接调用set方法。
这个模版类还是有许多方法的,所有的派生类方法列表都来自该模版类。

手动实现KVO

针对以上代码观察,我们来自己实现一波
为NSObject添加一个分类NSObject+KVOBlock来实现KVO的方法

- (void)sw_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
              callback:(sw_KVOObserverBlock)callback;

- (void)sw_removeObserver:(NSObject *)observer
               forKeyPath:(NSString *)keyPath;

这是大家熟悉的两个KVO方法,那么怎么具体实现呢?看过原理,代码当然还是不容易啊

- (void)sw_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
              callback:(sw_KVOObserverBlock)callback {
    // 1. 通过keyPath获取当前类对应的setter方法,如果获取不到,说明setter 方法即不存在与KVO类,也不存在与原始类,这总情况正常情况下是不会发生的,触发Exception
    NSString *setterString = sw_setterByGetter(keyPath);
    SEL setterSEL = NSSelectorFromString(setterString);
    Method method = class_getInstanceMethod(object_getClass(self), setterSEL);
    
    if (method) {
        // 2. 查看当前实例对应的类是否是KVO类,如果不是,则生成对应的KVO类,并设置当前实例对应的class是KVO类
        Class objectClass = object_getClass(self);
        NSString *objectClassName = NSStringFromClass(objectClass);
        if (![objectClassName hasPrefix:sw_KVOClassPrefix]) {
            Class kvoClass = [self makeKvoClassWithOriginalClassName:objectClassName];
            //改变当前obj的isa指向
            object_setClass(self, kvoClass);
        }
        
        // 3. 在KVO类中查找是否重写过keyPath 对应的setter方法,如果没有,则添加setter方法到KVO类中
        if (![self hasMethodWithMethodName:setterString]) {
            class_addMethod(object_getClass(self), NSSelectorFromString(setterString), (IMP)sw_kvoSetter, method_getTypeEncoding(method));
        }
        
        // 4. 注册Observer
        NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
        if (observerArray == nil) {
            observerArray = [NSMutableArray new];
            objc_setAssociatedObject(self, sw_KVOObserverAssociatedKey, observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        SWKVOObserverItem *item = [SWKVOObserverItem new];
        item.keyPath = keyPath;
        item.observer = observer;
        item.callback = callback;
        [observerArray addObject:item];
        NSLog(@"%@--observerArray----", observerArray);
        
        
    }else {
        NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
        NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
        [exception raise];
    }
}

- (void)sw_removeObserver:(NSObject *)observer
               forKeyPath:(NSString *)keyPath {
    NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
    [observerArray enumerateObjectsUsingBlock:^(SWKVOObserverItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.observer == observer && [obj.keyPath isEqualToString:keyPath]) {
            [observerArray removeObject:obj];
        }
    }];
    
    if (observerArray.count == 0) { // 如果已经没有了observer,则把isa复原,销毁临时的KVO类
        Class originalClass = [self class];
        Class kvoClass = object_getClass(self);//获得的是isa指向
        object_setClass(self, originalClass);
        objc_disposeClassPair(kvoClass);
    }
    
}

其中有一些小方法的实现细节,可以看ErenShi101/BlockKVO 其中根据KVO源码的原理,我们在手动实现时,需要注意的就是

  1. 在创建派生类时需要重写class方法和setter方法
  2. 通过一个观察者数组来添加被观察的对象,然后实现回调