iOS KVO的自我实现

代码下载

代码下载地址

系统KVO的使用

  1. KVO:是一种键值观察机制,当某个对象为某属性注册了观察后,只要该对象的此属性发生改变,就会通知观察者。

  2. 用KVO 实现如下两个效果:

屏幕快照 2017-03-30 下午4.52.32.png
屏幕快照 2017-03-30 下午4.52.42.png
  • 导航栏的透明度随着UITableView的滑动距离而变化(这个功能其实也可以使用UIScrollViewDelegate的代理方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView来实现)。
    (1) 为UITableView的contentOffset属性注册观察,其中context这个参数是为了区分父类是否对该消息感兴趣。
    [self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:KVOContext_ContentOffset];

(2)实现观察者回调方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if (context == KVOContext_ContentOffset) {
        if ([keyPath isEqualToString:@"contentOffset"]) {
            if (self.navBackView == nil) {
                UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, -20, [UIScreen mainScreen].bounds.size.width, 64)];
                view.backgroundColor = [UIColor orangeColor];
                [self.navigationController.navigationBar insertSubview:view atIndex:0];
                self.navBackView = view;
            }
            CGPoint contentOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
            CGFloat alpha = (contentOffset.y - 64)*(1/136.0);
            
            if (alpha >= 1) {
                self.navBackView.alpha = 1;
            }
            else if (alpha > 0 && alpha < 1)
            {
                self.navBackView.alpha = alpha;
            }
            else
            {
                self.navBackView.alpha = 0;
            }
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

(3)在观察者销毁的时候移除掉观察

- (void)dealloc
{
    [self removeObserver:self.tableView forKeyPath:@"contentOffset"];
}

说明:导航栏没有设置透明度的方法和属性,需要先设置导航栏的背景图和阴影图为空的图片来设置导航栏的透明,接着向导航栏插入一个视图,通过控制该视图的透明度来达到导航栏的控制导航栏的透明度效果。

    [self.navigationController.navigationBar setBackgroundImage:[[UIImage alloc] init] forBarMetrics:UIBarMetricsDefault];
    [self.navigationController.navigationBar setShadowImage:[[UIImage alloc] init]];
  • 为UITableView添加多个cell,每个cell上都有一个时间不一样的倒计时。
    (1)先包装一个时间数据模型
#import <Foundation/Foundation.h>

@interface TimeModel : NSObject

@property (copy, nonatomic) NSString *title;
@property (assign, nonatomic) NSInteger time;

@end

(2)在cell中增加一个TimeModel属性,并在设置属性的时候,注册观察,实现观察者回调方法

- (void)setTimeModel:(TimeModel *)timeModel
{
    if (timeModel) {
        [timeModel addObserver:self forKeyPath:@"time" options:NSKeyValueObservingOptionNew context:KVOContext];
        self.textLabel.text = [NSString stringWithFormat:@"%@倒计时:%i", timeModel.title, (int)timeModel.time];
        
        if (_timeModel) {
            [_timeModel removeObserver:self forKeyPath:@"time"];
        }
        _timeModel = timeModel;
    }
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == KVOContext) {
        if ([keyPath isEqualToString:@"time"]) {
            self.textLabel.text = [NSString stringWithFormat:@"%@倒计时:%i", self.timeModel.title, [change[NSKeyValueChangeNewKey] intValue]];
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

(3)在控制器中用一个定时器来更改倒计时的时间

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- (void)timerAction:(NSTimer *)timer
{
    for (TimeModel *timeModel in self.dataArr) {
        if (timeModel.time > 0) {
            timeModel.time--;
        }
    }
}

注意:
<1>在cell中设置时间模型的时候,因为cell是重用的,如果cell中存在时间模型,得先移除掉对该时间模型的观察,再为新赋值的时间模型注册观察。
<2>只要设置了观察者,就必须移除,在观察者销毁的时候移除掉所有观察。

KVO的实现原理

当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPath的setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVO为NSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。

(isa 指针的作用:每个对象都有isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。) 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。

KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用2个方法:

被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

KVO的自我实现

  • 创建一个类,用于存储观察者,观察的属性,以及观察者的回调方法。
@interface QSPKVOInfo : NSObject

@property (weak, nonatomic) id observer;
@property (copy, nonatomic) NSString *key;
@property (copy, nonatomic) KVOBlock block;

@end

@implementation QSPKVOInfo

+ (instancetype)QSPKVOInfo:(id)observer key:(NSString *)key block:(KVOBlock)block
{
    return [[self alloc] initWithObserver:observer key:key block:block];
}
- (instancetype)initWithObserver:(id)observer key:(NSString *)key block:(KVOBlock)block
{
    if (self = [super init]) {
        self.observer = observer;
        self.key = key;
        self.block = block;
    }
    
    return self;
}

@end
  • 创建一个NSObject的分类,并添加一个添加观察者和一个移除观察者的方法
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef void (^KVOBlock)(id object, id observer, NSString *key, CGPoint oldValue, CGPoint newValue);

@interface NSObject (KVO)

- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block;
- (void)QSP_removeObserver:(NSObject *)observer forkey:(NSString *)key;

@end
  • 实现添加观察者的方法- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block
- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block
{
    //1.检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
    Class class = [self class];
    SEL setterSelector = NSSelectorFromString(setterForGeter(key));
    Method setterMethod = class_getInstanceMethod(class, setterSelector);
    if (!setterMethod) {
        NSLog(@"%@属性不存在!", key);
        return;
    }
    
    //2.检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;
    NSString *className = NSStringFromClass(class);
    if (![className hasPrefix:KVOClassPrefix]) {
        class = [self makeKvoClassWithOriginalClassName:className];
        object_setClass(self, class);
    }
    NSLog(@"%@", [self class]);
    
    //3.检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
    if (![self hasSelector:setterSelector]) {
        const char *methodTypes = method_getTypeEncoding(class_getInstanceMethod(class, setterSelector));
        NSLog(@"%s", methodTypes);
        BOOL success = NO;
        NSLog(@"valueClass:%@", [[self valueForKey:key] class]);
        if ([[self valueForKey:key] isKindOfClass:[NSObject class]]) {
            success = class_addMethod(class, setterSelector, (IMP)kvo_setter, methodTypes);
        }
        if (success) {
            NSLog(@"重写%@方法成功!", NSStringFromSelector(setterSelector));
        }
        else
        {
            NSLog(@"重写%@方法失败!", NSStringFromSelector(setterSelector));
        }
    }
    
    //4.添加这个观察者
    NSMutableDictionary *infoDic = objc_getAssociatedObject(self, KVOInfoDictionaryName.UTF8String);
    if (infoDic == nil) {
        infoDic = [NSMutableDictionary dictionaryWithCapacity:1];
        objc_setAssociatedObject(self, KVOInfoDictionaryName.UTF8String, infoDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    QSPKVOInfo *info = [QSPKVOInfo QSPKVOInfo:observer key:key block:block];
    infoDic[key] = info;
}
  • 实现几个辅助方法
/**
 根据getter方法名获取setter方法名

 @param key getter方法名
 @return setter方法名
 */
NSString * setterForGeter(NSString *key)
{
    if (key && key.length > 0) {
        NSString *upperKey = [key uppercaseString];
        return [NSString stringWithFormat:@"set%@%@:", [upperKey substringToIndex:1], [key substringFromIndex:1]];
    }
    
    return nil;
}

/**
 根据setter方法名获取getter方法名

 @param setter setter方法名
 @return getter方法名
 */
NSString * getterForSetter(NSString *setter)
{
    if ([setter hasPrefix:@"set"] && [setter hasSuffix:@":"]) {
        NSString *lowerKey = [setter lowercaseString];
        return [NSString stringWithFormat:@"%@%@", [lowerKey substringWithRange:NSMakeRange(3, 1)], [setter substringWithRange:NSMakeRange(4, setter.length - 5)]];
    }
    
    return nil;
}

/**
 kvo类class方法的IMP
 */
Class kvo_class(id self, SEL _cmd)
{
//    return object_getClass(self);
    return class_getSuperclass(object_getClass(self));
}

/**
 判断类中是否存在某个方法

 @param selector SLE
 */
- (BOOL)hasSelector:(SEL)selector
{
    unsigned int methodListCount = 0;
    Method *methodList = class_copyMethodList(object_getClass(self), &methodListCount);
    for (int index = 0; index < methodListCount; index++) {
        NSLog(@"%@", NSStringFromSelector(method_getName(methodList[index])));
        if (selector == method_getName(methodList[index])) {
            return YES;
        }
    }
    
    return NO;
}

/**
 kvo类的setter方法的IMP
 */
void kvo_setter(id self, SEL _cmd, CGPoint value)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        NSLog(@"%@属性不存在!", getterName);
    }
    
    id oldValue = [self valueForKey:getterName];
    NSLog(@"oldValue:%@", oldValue);
    NSLog(@"newValue:%@", NSStringFromCGPoint(value));
    
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    void (*objc_msgSendSuperCasted)(void *, SEL, CGPoint) = (void *)objc_msgSendSuper;
    objc_msgSendSuperCasted(&superclazz, _cmd, value);
    
    NSMutableDictionary *infoDic = objc_getAssociatedObject(self, KVOInfoDictionaryName.UTF8String);
    QSPKVOInfo *info = infoDic[getterName];
    if (info.block) {
        info.block(self, info.observer, info.key, [oldValue CGPointValue], value);
    }
}

/**
 根据类名创建kvo类
 
 @param originalClazzName 类名
 @return kvo类
 */
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClassName = [KVOClassPrefix stringByAppendingString:originalClazzName];
    Class kvoClass = NSClassFromString(kvoClassName);
    if (kvoClass) {
        return kvoClass;
    }
    
    Class originalClass = [self class];
    kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
    BOOL success = class_addMethod(kvoClass, @selector(class), (IMP)kvo_class, method_getTypeEncoding(class_getClassMethod(originalClass, @selector(class))));
    if (success) {
        NSLog(@"重写class方法成功!");
    }
    else
    {
        NSLog(@"重写class方法失败!");
    }
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
}
  • 实现移除观察者的方法
- (void)QSP_removeObserver:(NSObject *)observer forkey:(NSString *)key
{
    NSMutableDictionary *infoDic = objc_getAssociatedObject(self, KVOInfoDictionaryName.UTF8String);
    QSPKVOInfo *info = infoDic[key];
    
    if ([info.key isEqualToString:key] && info.observer == observer) {
        [infoDic removeObjectForKey:key];
    }
}

说明:在这里我只实现了对CGPoint属性的,如果真的要实现一个KVO框架的话,还需定义一个数据模型出来,用来承载任何类型的属性,并能够解析出对应的数据。

使用自定义的KVO

使用上面的第一个示例,对UITableView的contentOffset属性进行监听,实现对导航栏透明度的控制

[self.tableView QSP_addObserver:self forkey:@"contentOffset" withBlock:^(id object, id observer, NSString *key, CGPoint oldValue, CGPoint newValue) {
        if (self.navBackView == nil) {
            UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, -20, [UIScreen mainScreen].bounds.size.width, 64)];
            view.backgroundColor = [UIColor orangeColor];
            [self.navigationController.navigationBar insertSubview:view atIndex:0];
            self.navBackView = view;
        }
        
        CGPoint contentOffset = newValue;
        CGFloat alpha = (contentOffset.y - 64)*(1/136.0);
        
        if (alpha >= 1) {
            self.navBackView.alpha = 1;
        }
        else if (alpha > 0 && alpha < 1)
        {
            self.navBackView.alpha = alpha;
        }
        else
        {
            self.navBackView.alpha = 0;
        }
    }];

记得在dealoc方法中移除观察

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

推荐阅读更多精彩内容

  • 本文分为2个部分:概念与应用。概念部分旨在剖析 KVO 这一设计模式的实现原理;应用部分通过创建的项目,以说明 K...
    啊左阅读 57,119评论 107 437
  • iOS--KVO的实现原理与具体应用 长时间不用容易忘,这篇文章挺好的.转载自看本文分为2个部分:概念与应用。概念...
    超_iOS阅读 1,420评论 0 17
  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 9,984评论 11 33
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,636评论 0 9
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 2,983评论 0 26