KVO应用、原理及自实现

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


qrcode_for_gh_1b8d7fd76d13_430.jpg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容