美文网首页IOS开发谈杂iOS开发
KVO应用、原理及自实现

KVO应用、原理及自实现

作者: 天涯一梦 | 来源:发表于2017-11-14 20:18 被阅读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原理

    • 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

    相关文章

      网友评论

      本文标题:KVO应用、原理及自实现

      本文链接:https://www.haomeiwen.com/subject/mptqvxtx.html