一.KVO简介
KVO 是ios里,观察者设计模式的一种应用实现,依赖runtime,基于KVC,KVO提供了一种机制,可以监听类的属性,当被监听的属性发生变化时,监听者或叫观察者会获得通知,然后就可以做出相应的逻辑处理。例如,我们在售票系统中,监听存票的变化,当有客户购票或退票的时候,我们获取变化,然后操作数据库,出票或存票。
二.KVO用法
1.添加监听
-(void)addObserver:(NSObject*)observerforKeyPath:(NSString*)keyPathoptions:(NSKeyValueObservingOptions)optionscontext:(nullablevoid*)context;(系统还有其他添加方法)
2.接收通知
-(void)observeValueForKeyPath:(nullableNSString*)keyPathofObject:(nullableid)objectchange:(nullableNSDictionary *)changecontext:(nullablevoid*)context
3.移除监听
- (void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath context:(nullablevoid*)context (系统还有其他移除方法)
4.示例代码
头文件:
//
// ViewController.h
// LiveMeUI
//
// Created by cheng chuanpeng on 06/09/2017.
// Copyright © 2017 cheng chuanpeng. All rights reserved.
//
#import
@interfaceViewController:UIViewController
@end
/**********************************分割线**********************************/
//测试类
@interfaceTicketModel:NSObject
@property(nonatomic,assign)NSIntegerticketCount;
- (void)rollbackTick:(NSString*)ticketId;
- (NSString*)outTicket;
@end
.m文件
#import"ViewController.h"
@interfaceViewController(){
TicketModel *_ticketModel;
}
@property(weak,nonatomic)IBOutletUIView*bgView;
@end
@implementationViewController
- (void)viewDidLoad {
[superviewDidLoad];
_ticketModel = [[TicketModel alloc]init];
[_ticketModel addObserver:selfforKeyPath:@"ticketCount"options:NSKeyValueObservingOptionNewcontext:nil];
NSString* ticketID = [_ticketModel outTicket];
NSLog(@"张三买到了票,ID=%@",ticketID);
[_ticketModel rollbackTick:ticketID];
NSLog(@"张三把票退了,ID=%@",ticketID);
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{
NSNumber*newValue = change[NSKeyValueChangeNewKey];
NSLog(@"KVO监听到票数变化,最新票数为:%@",newValue);
}
- (void)dealloc{
//移除监听
[_ticketModel removeObserver:selfforKeyPath:@"ticketCount"];
}
- (void)didReceiveMemoryWarning {
[superdidReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
@implementationTicketModel
- (instancetype)init{
if(self==[superinit]) {
_ticketCount =1;//只有一张票,123456
}
returnself;
}
- (void)rollbackTick:(NSString*)ticketId{
self.ticketCount ++;
NSLog(@"退票,票号:%@",ticketId);
}
- (NSString*)outTicket{
self.ticketCount --;
NSLog(@"出票,票号:123456");
return@"123456";
}
@end
控制台打印
2017-11-14 15:06:51.872816+0800 LiveMeUI[6343:2498635] KVO监听到票数变化,最新票数为:0
2017-11-14 15:06:51.872920+0800 LiveMeUI[6343:2498635] 出票,票号:123456
2017-11-14 15:06:51.872934+0800 LiveMeUI[6343:2498635] 张三买到了票,ID=123456
2017-11-14 15:06:51.872956+0800 LiveMeUI[6343:2498635] KVO监听到票数变化,最新票数为:1
2017-11-14 15:06:51.872967+0800 LiveMeUI[6343:2498635] 退票,票号:123456
2017-11-14 15:06:51.872977+0800 LiveMeUI[6343:2498635] 张三把票退了,ID=123456
讲解:如上所示,当我们初始化TicketModel以后,我默认系统只有一张票,票号为123456,当调用outTicket时,123456这张票,被买走,此时,系统无存票,票数为0,kvo监听到变化,打印出来结果,退票逻辑也是一样的,这样,我们监听了ticketModel里的ticketCount之后,我们通过kvo就可以监听到票数的变化 ,如果有多个窗口,即多处调用,我们不用关心具体哪个窗口在买票或退票,只要有票数变化 ,我们就可以收到通知,然后做出处理,这就是kvo的一个典型应用。(想一下,TicketModel改成TicketCent,做成单例,分发给多个窗口使用,即不同的类或对象调用TicketCent卖票或退票,我们完全可以不必关心窗口,我们只关心通知结果就可以了,当然,线程安全我们没有处理,这不是本文重点,可以先忽略)
三.KVO原理
1.当一个类的属性被观察的时候,系统会通过runtime动态的创建一个该类的派生类,并添加观察者
2.在派生类中重写基类被观察的属性的setter方法,当setter被调用的时候,在赋值之前调用willChangeValueForKey方法通知观察者,赋值之后,调用didChangeValueForKey方法通知观察者,didChangeValueForKey会触发observeValueForkeyPath方法,当然其他相关的方法也会在此时被通知到观察者,感兴趣可以自己查api.
3.交换派生类和基类的ias指针
4.重写class方法
说明:重写setter方法是为了通知观察者,为什么要交换isa指针呢?是因为方法或属性或变量寻址,是通过isa指针开始的,所以要交换isa指针,这样当属性变化时,才可以通知到观察者,重写class方法,是因为,,,,,apple公司不想让我们知道的太多!!!所以当我们用class方法拿到的结果,是基类,而不是派生类,不过没有关系,我们同样有其他手段可以验证。
验证KVO原理:
首先,我们在上面的viewcontroller.m里面,添加如下两个方法
staticNSArray *ClassMethodNames(Class c)
{
NSMutableArray *array= [NSMutableArrayarray];
unsignedintmethodCount =0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsignedinti;
for(i =0; i < methodCount; i++)
[arrayaddObject: NSStringFromSelector(method_getName(methodList[i]))];
free(methodList);
returnarray;
}
staticvoidPrintClassInfo(id obj)
{
NSString *str = [NSString stringWithFormat:
@"%@\n\tclassName: %s\n\tclsss isa: %s\n\timplements methods <%@>",
obj,
class_getName([objclass]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
然后,我们在添加KVO观察前后,分别添加打印,代码位置如下,
_ticketModel = [[TicketModel alloc]init];
PrintClassInfo(_ticketModel);
[_ticketModel addObserver:selfforKeyPath:@"ticketCount"options:NSKeyValueObservingOptionNewcontext:nil];
PrintClassInfo(_ticketModel);
NSString* ticketID = [_ticketModel outTicket];
NSLog(@"张三买到了票,ID=%@",ticketID);
[_ticketModel rollbackTick:ticketID];
NSLog(@"张三把票退了,ID=%@",ticketID);
控制台输出:
className: TicketModel
clsss isa:TicketModel
implementsmethods
className:TicketModel
clsss isa:NSKVONotifying_TicketModel
implements methods
2017-11-14 15:58:17.614993+0800 LiveMeUI[6364:2518301] KVO监听到票数变化,最新票数为:0
2017-11-14 15:58:17.615037+0800 LiveMeUI[6364:2518301] 出票,票号:123456
2017-11-14 15:58:17.615062+0800 LiveMeUI[6364:2518301] 张三买到了票,ID=123456
2017-11-14 15:58:17.615102+0800 LiveMeUI[6364:2518301] KVO监听到票数变化,最新票数为:1
2017-11-14 15:58:17.615115+0800 LiveMeUI[6364:2518301] 退票,票号:123456
2017-11-14 15:58:17.615126+0800 LiveMeUI[6364:2518301] 张三把票退了,ID=123456
- 分析:如控制台打印所示,当没有添加KVO观察时,className是TicketModel,isa也是指向TicketModel类,方法有我们写的两个outTicket,roobackTick以及系统帮我们生成的ticketCount的getter和setter方法,到这里,一切都是正常的,但是,当我们使用了KVO之后,通过打印发现,isa变成了NSKVONotifying_TicketModel,NSKVONotifying_TicketModel是我们的派生类,派生类会在基类的名字前加NSKVONotifying_,此时,isa交换完成,并且,实现方法里可以看到,只有被我们实现的监听属性ticketCount的setter方法,致此,我们可以发现,打印完成验证了我们上面的结论,验证完成。
四.自定义实现KVO
了解了kvo的原理之后,我们可以尝试自己实现一套kvo实现机制,添加我们一些kvo没有的实现,比如,我们不想在oberserveforpath里面处理,我们想在添加kvo的时候,直接在block里面处理,这是kvo现有api里没有的,当然你也可以添加别的api
实现过程:
1.添加NSObject的Category,添加addOberserver方法跟removeObserver方法,因为我们要加block回传数据变化 ,所有observerForKeyPath我们不做实现.
2.检查对象的isa指向的类是不是一个KVO类型。如果不是,新建一个派生类,派生类继承基类,交换派生类与基类的isa指针。
3.重写派生类的setter方法。
4.添加观众者
5.重写class方法
6.remove的时候,去掉观察者,注意:这个时候,要把isa指针交换回来。
代码参考网络,本身的代码有一些问题,我已做修改,如下:
.h文件
//
// NSObject+KVO.h
// ImplementKVO
//
// Created by cheng chuanpeng on 06/09/2017.
// Copyright © 2017 cheng chuanpeng. All rights reserved.
//
#import
typedefvoid(^TYYMObservingBlock)(idobservedObject,NSString*observedKey,idoldValue,idnewValue);
@interfaceNSObject(KVO)
- (void)TYYM_addObserver:(NSObject*)observer
forKey:(NSString*)key
withBlock:(TYYMObservingBlock)block;
- (void)TYYM_removeObserver:(NSObject*)observer forKey:(NSString*)key;
@end
.m实现文件
//
// NSObject+KVO.m
// ImplementKVO
//
// Created by cheng chuanpeng on 06/09/2017.
// Copyright © 2017 cheng chuanpeng. All rights reserved.
//
#import"NSObject+KVO.h"
#import
#import
NSString*constkTYYMKVOClassPrefix =@"TYYMKVOClassPrefix_";
NSString*constkTYYMKVOAssociatedObservers =@"TYYMKVOAssociatedObservers";
#pragma mark - TYYMObservationInfo
@interfaceTYYMObservationInfo:NSObject
@property(nonatomic,weak)NSObject*observer;
@property(nonatomic,copy)NSString*key;
@property(nonatomic,copy) TYYMObservingBlock block;
@end
@implementationTYYMObservationInfo
- (instancetype)initWithObserver:(NSObject*)observer Key:(NSString*)key block:(TYYMObservingBlock)block
{
self= [superinit];
if(self) {
_observer = observer;
_key = key;
_block = block;
}
returnself;
}
@end
#pragma mark - Debug Help Methods
staticNSArray*ClassMethodNames(Class c)
{
NSMutableArray*array = [NSMutableArrayarray];
unsignedintmethodCount =0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsignedinti;
for(i =0; i < methodCount; i++) {
[array addObject:NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
returnarray;
}
staticvoidPrintDescription(NSString*name,idobj)
{
NSString*str = [NSStringstringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tRuntime class %s\n\timplements methods <%@>\n\n",
name,
obj,
class_getName([objclass]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
#pragma mark - Helpers
staticNSString* getterForSetter(NSString*setter)
{
if(setter.length <=0|| ![setterhasPrefix:@"set"] || ![setterhasSuffix:@":"]) {
returnnil;
}
// remove 'set' at the begining and ':' at the end
NSRangerange =NSMakeRange(3,setter.length -4);
NSString*key = [settersubstringWithRange:range];
// lower case the first letter
NSString*firstLetter = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0,1)
withString:firstLetter];
returnkey;
}
staticNSString* setterForGetter(NSString*getter)
{
if(getter.length <=0) {
returnnil;
}
// upper case the first letter
NSString*firstLetter = [[gettersubstringToIndex:1] uppercaseString];
NSString*remainingLetters = [gettersubstringFromIndex:1];
// add 'set' at the begining and ':' at the end
NSString*setter= [NSStringstringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
returnsetter;
}
#pragma mark - Overridden Methods
staticvoidkvo_setter(idself, SEL _cmd,idnewValue)
{
NSString*setterName =NSStringFromSelector(_cmd);
NSString*getterName = getterForSetter(setterName);
if(!getterName) {
NSString*reason = [NSStringstringWithFormat:@"Object %@ does not have setter %@",self, setterName];
@throw[NSExceptionexceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
idoldValue = [selfvalueForKey:getterName];
structobjc_super superclazz = {
.receiver =self,
.super_class = class_getSuperclass(object_getClass(self))
};
// cast our pointer so the compiler won't complain
void(*objc_msgSendSuperCasted)(void*, SEL,id) = (void*)objc_msgSendSuper;
// call super's setter, which is original class's setter method
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// look up observers and call the blocks
NSMutableArray*observers = objc_getAssociatedObject(self, (__bridgeconstvoid*)(kTYYMKVOAssociatedObservers));
for(TYYMObservationInfo *eachinobservers) {
if([each.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
each.block(self, getterName, oldValue, newValue);
});
}
}
}
staticClass kvo_class(idself, SEL _cmd)
{
returnclass_getSuperclass(object_getClass(self));
}
#pragma mark - KVO Category
@implementationNSObject(KVO)
- (void)TYYM_addObserver:(NSObject*)observer
forKey:(NSString*)key
withBlock:(TYYMObservingBlock)block
{
SEL setterSelector =NSSelectorFromString(setterForGetter(key));
Method setterMethod = class_getInstanceMethod([selfclass], setterSelector);
if(!setterMethod) {
NSString*reason = [NSStringstringWithFormat:@"Object %@ does not have a setter for key %@",self, key];
@throw[NSExceptionexceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
Class clazz = object_getClass(self);
NSString*clazzName =NSStringFromClass(clazz);
// if not an KVO class yet
if(![clazzName hasPrefix:kTYYMKVOClassPrefix]) {
clazz = [selfmakeKvoClassWithOriginalClassName:clazzName];
object_setClass(self, clazz);
}
// add our kvo setter if this class (not superclasses) doesn't implement the setter?
if(![selfhasSelector:setterSelector]) {
constchar*types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}
TYYMObservationInfo *info = [[TYYMObservationInfo alloc] initWithObserver:observer Key:key block:block];
NSMutableArray*observers = objc_getAssociatedObject(self, (__bridgeconstvoid*)(kTYYMKVOAssociatedObservers));
if(!observers) {
observers = [NSMutableArrayarray];
objc_setAssociatedObject(self, (__bridgeconstvoid*)(kTYYMKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:info];
}
- (void)TYYM_removeObserver:(NSObject*)observer forKey:(NSString*)key
{
NSMutableArray* observers = objc_getAssociatedObject(self, (__bridgeconstvoid*)(kTYYMKVOAssociatedObservers));
TYYMObservationInfo *infoToRemove;
for(TYYMObservationInfo* infoinobservers) {
if(info.observer == observer && [info.key isEqual:key]) {
infoToRemove = info;
break;
}
}
[observers removeObject:infoToRemove];
}
- (Class)makeKvoClassWithOriginalClassName:(NSString*)originalClazzName
{
NSString*kvoClazzName = [kTYYMKVOClassPrefix stringByAppendingString:originalClazzName];
Class clazz =NSClassFromString(kvoClazzName);
if(clazz) {
returnclazz;
}
// class doesn't exist yet, make it
Class originalClazz = object_getClass(self);
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String,0);
// grab class method's signature so we can borrow it
Method clazzMethod = class_getInstanceMethod(originalClazz,@selector(class));
constchar*types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz,@selector(class), (IMP)kvo_class, types);
objc_registerClassPair(kvoClazz);
returnkvoClazz;
}
- (BOOL)hasSelector:(SEL)selector
{
Class clazz = object_getClass(self);
unsignedintmethodCount =0;
Method* methodList = class_copyMethodList(clazz, &methodCount);
for(unsignedinti =0; i < methodCount; i++) {
SEL thisSelector = method_getName(methodList[i]);
if(thisSelector == selector) {
free(methodList);
returnYES;
}
}
free(methodList);
returnNO;
}
@end
调用示例
_ticketModel = [[TicketModel alloc]init];
PrintClassInfo(_ticketModel);
[_ticketModel TYYM_addObserver:selfforKey:@"ticketCount"withBlock:^(idobservedObject,NSString*observedKey,idoldValue,idnewValue) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"自定义KVO实现,oldValue= %@,newValue=%@",(NSString*)oldValue,(NSString*)newValue);
});
}];
PrintClassInfo(_ticketModel);
控制台输出
className: TicketModel
clsss isa: TicketModel
implements methods
className: TicketModel
clsss isa: TYYMKVOClassPrefix_TicketModel
implements methods
2017-11-14 19:29:00.553276+0800 LiveMeUI[6502:2608844] 出票,票号:123456
2017-11-14 19:29:00.553316+0800 LiveMeUI[6502:2608844] 张三买到了票,ID=123456
2017-11-14 19:29:00.553357+0800 LiveMeUI[6502:2608844] 退票,票号:123456
2017-11-14 19:29:00.553368+0800 LiveMeUI[6502:2608844] 张三把票退了,ID=123456
2017-11-14 19:29:00.570220+0800 LiveMeUI[6502:2608844] 自定义KVO实现,oldValue= 1,newValue=0
2017-11-14 19:29:00.570267+0800 LiveMeUI[6502:2608844] 自定义KVO实现,oldValue= 0,newValue=1
注意:大家注意看下,"自定义KVO实现XXXX"这两条log顺序与出票顺序,大家可以考虑下是什么原因?另外,大家可以考虑下,这样实现会不会有问题?会有什么问题?除了这种实现,还有没有别的实现方式?
好了,以上算是留给大家的小思考题吧,欢迎大家提出宝贵意见,欢迎大家关注微信公众号:IOS开发杂谈