iOS中的键值编码(KVC)与观察(KVO)(三)

/ 0评 / 0

KVO与容器类

对容器类的观察与对非容器类的观察并不一样,不可变容器的内容发生改变并不会影响他们所在的容器,可变容器的内容改变(内容增删)也都不会影响所在的容器,如果要观察容器中对象的改变,必须观察那些对象,而不是容器本身。那么如果我们需要观察某容器中的对象,首先我们得观察容器内容的变化,在容器内容增加时添加对新内容的观察,在内容移除同时移除对该内容的观察。

  
//我们通过KVC拿到容器属性的代理对象
NSMutableArray *threeTimes = [threeTimesArray mutableArrayValueForKey:@"numbers"];  
[threeTimes addObject:@"这是一个数据"];

当然这样做的前提是要实现 insertObject:inValAtIndex: 和 removeObjectFromValAtIndex:两个方法。如此才能触发 observeValueForKeyPath:ofObject:change:context: 的响应。
而后,我们就可以轻而易举地在那两个方法实现内对容器新成员添加观察/对容器废弃成员移除观察。

KVO的实现原理

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey: ,在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值,而当改变发生后 didChangeValueForKey: 会被调用,继而 observeValueForKey: ofObject: change : context: 也会被调用,可以手动实现这些调用,但是俺们又不傻,一般我们只在希望能控制回调的调用时机时才会这么做。

kvc&&kvo002

在调用setNow: 时,系统还会以某种方式在中间插入 willChangeValueForKey:、 didChangeValueForKey: 和 observeValueForKey: ofObject: change : context: 的调用,有时候我们也会看到人们这么写:

  
- (void)setNow:(nsDate *)aDate{
	[self willChangeValueForKey:@"now"]; //没必要
	_now = aDate;
	[self didChangeValueForKey:@"now"];//没必要
}

这是没有必要的代码,这样的话KVO 代码会被调用两次,KVO在调用存取方法之前总是调用 willChangeValueForKey: , 之后总是调用 didChangeValueForKey: ,那这是怎么做到的呢?答案是通过混写,这个以后介绍。第一次对一个对象调用 addObserver: forKeyPath: options: context: 时,框架会创建这个类的新的KVO子类,并将观察对象转换为新的子类对象,在这个KVO特殊子类中,Cocoa创建观察属性的设置方法 的大致原理如下:

  
- (void)setNow:(nsDate *)aDate{
	[self willChangeValueForKey:@"now"];
	[super setValue:aDate forKey:@"now"];
	[self didChangeValueForKey:@"now"];
}

这种继承和方法注入是在运行时而不是编译时实现的,这就是正确命名重要的原因,所以必须按照规则来命名。

我们来试验一下,先来准备一个新子类的setter方法:

  
- (void)notifySetter:(id)newValue {
    	NSLog(@"我是新的setter");
}

setter的实现先留空,下面再详细说,紧接着,我们直接进入主题,runtime注册一个新类,并且让被监听类的isa指针指向我们自己伪造的类,为了大家看得方便,暂时就不做封装了,所有直接写在一个方法内:

  
- (Class)configureKVOSubClassWithSourceClassName:(NSString *)className observeProperty:(NSString *)property {
    NSString *prefix = @"NSKVONotifying_";
    NSString *subClassName = [prefix stringByAppendingString:className];
 
    //1
    Class originClass = [KVOTargetClass class];
    Class dynaClass = objc_allocateClassPair(originClass, subClassName.UTF8String, 0);
 
    //重写property对应setter
    NSString *propertySetterString = [@"set" stringByAppendingString:[[property substringToIndex:1] uppercaseString]];
    propertySetterString = [propertySetterString stringByAppendingString:[property substringFromIndex:1]];
    propertySetterString = [propertySetterString stringByAppendingString:@":"];
    SEL setterSEL = NSSelectorFromString(propertySetterString);
 
    //2
    Method setterMethod = class_getInstanceMethod(originClass, setterSEL);
    const char types = method_getTypeEncoding(setterMethod);
    class_addMethod(dynaClass, setterSEL, class_getMethodImplementation([self class], @selector(notifySetter:)), types);
 
    objc_registerClassPair(dynaClass);
    return dynaClass;
}

我们来看
//1处,我们要创建一个新的类,可以通过objc_allocateClassPair来创建这个新类和他的元类,第一个参数需提供superClass的类对象,第二个参数接受新类的类名,类型为const char *,通过返回值我们得到dynaClass类对象。

//2处,我们希望为我们的伪造的类添加跟被观察类一样只能的setter方法,我们可以借助被观察类,拿到类型编码信息,通过class_addMethod,注入我们自己的setter方法实现:class_getMethodImplementation([self class], @selector(notifySetter:)),最后通过objc_registerClassPair完成新类的注册!。
可能有朋友会问class_getMethodImplementation中获取IMP的来源[self class]的self是指代什么?其实就是指代我们自己的setter(notifySetter:)IMP实现所在的类,指代从哪个类可以找到这个IMP,笔者这里是直接开一个新工程,在ViewController里就开干的,notifySetter:和这个手术方法configureKVOSubClassWithSourceClassName: observeProperty:所在的地方就是VC,因此self指向的就是这个VC实例,也就是这个手术方法的调用者。

不用怀疑,经过手术后对KVOTargetClass对应属性的修改,就会进入到我们伪装的setter,下面我们来完成先前留空的setter实现:

  
- (void)notifySetter:(id)newValue {
    NSLog(@"我是新的setter");
 
    struct objc_super originClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
 
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *propertyName = [setterName substringFromIndex:3];
    propertyName = [[propertyName substringToIndex:propertyName.length - 1] lowercaseString];
 
    [self willChangeValueForKey:propertyName];
    //调用super的setter
    //1
    void (*objc_msgSendSuperKVO)(void * class, SEL _cmd, id value) = (void *)objc_msgSendSuper;
    //2
    objc_msgSendSuperKVO(&originClass, _cmd, newValue);
    [self didChangeValueForKey:propertyName];
}

我们轻而易举地让willChangeValueForKey:和didChangeValueForKey:包裹了对newValue的修改。

这里需要提的是:

//1处,在IOS8后,我们不能直接使用objc_msgSend()或者objc_msgSendSuper()来发送消息,我们必须自定义一个msg_Send函数并提供具体类型来使用。

//2处,至于objc_msgSendSuper(struct objc_super *, SEL, ...),第一个参数我们需要提供一个objc_super结构体,我们command跳进去来看看这个结构体:

  
/// Specifies the superclass of an instance. 
struct objc_super {  
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;
 
    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

第一个成员receiver表示某个类的实例,第二个成员super_class代表当前类的父类,也就是这里接受消息目标的类。

工作已经完成了,可以随便玩了:

  
- (void)main {
    KVOTargetClass *kvoObject = [[KVOTargetClass alloc] init];
    NSString *targetClassName = NSStringFromClass([KVOTargetClass class]);
    Class subClass = [self configureKVOSubClassWithSourceClassName:targetClassName observeProperty:@"name"];
    object_setClass(kvoObject, subClass);
 
    [kvoObject setName:@"haha"];
    NSLog(@"property -- %@", kvoObject.name);
}

KVO的使用选择

KVO 是提供对象属性被改变时的通知的机制。KVO 的实现在 Foundation 中,很多基于 Foundation 的框架都依赖它。
如果只对某个对象的值的改变感兴趣的话,就可以使用 KVO 消息传递。不过有一些前提:第一,接收者(接收对象改变的通知的对象)需要知道发送者 (值会改变的对象);第二,接收者需要知道发送者的生命周期,因为它需要在发送者被销毁前注销观察者身份。如果这两个要去符合的话,这个消息传递机制可以一对多(多个观察者可以注册观察同一个对象的变化)
如果要在 Core Data 上使用 KVO 的话,方法会有些许差别。这和 Core Data 的惰性加载 (faulting) 机制有关。一旦一个 managed object 被惰性加载处理的话,即使它的属性没有被改变,它还是会触发相应的观察者。把属性值先取入缓存中,在对象需要的时候再进行一次访问,这在 Core Data 中是默认行为,这种技术称为 Faulting。这么做可以避免降低内存开销,但是如果你确定将访问结果对象的具体属性值时,可以禁用 Faults 以提高获取性能。

KVO的权衡

KVO是强大的技术,除了可能比直接调用方法稍慢外,一般来说是个好东西,不足之处就是编译时不会检查,写代码时总是应该遵循KVC的命名规范。另一方面KVO也是好坏参半,可能很好用,也可能带来麻烦,有时候会出现很多莫名其妙的错误,所以KVO得bug都很难解决,因为很多行为就是发生了,但是却没有任何可见的代码说明发生行为的原因。所以在使用KVO时要尽量简单、保守,要只在真正带来好处的地方使用。在存在复杂相互依赖关系或者复杂的类继承层次的地方尽量避免使用KVO,用委托和通知这种简单的解决方案会更好。

评论已关闭。