IOS开发谈杂iOS开发

KVO应用、原理及自实现

2017-11-14  本文已影响108人  天涯一梦

一.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原理

首先,我们在上面的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

了解了kvo的原理之后,我们可以尝试自己实现一套kvo实现机制,添加我们一些kvo没有的实现,比如,我们不想在oberserveforpath里面处理,我们想在添加kvo的时候,直接在block里面处理,这是kvo现有api里没有的,当然你也可以添加别的api

实现过程:

代码参考网络,本身的代码有一些问题,我已做修改,如下:

.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开发杂谈


qrcode_for_gh_1b8d7fd76d13_430.jpg
上一篇 下一篇

猜你喜欢

热点阅读