iOS KVO的自我實現(xiàn)

代碼下載

代碼下載地址

系統(tǒng)KVO的使用

  1. KVO:是一種鍵值觀察機制颂鸿,當某個對象為某屬性注冊了觀察后,只要該對象的此屬性發(fā)生改變攒庵,就會通知觀察者嘴纺。

  2. 用KVO 實現(xiàn)如下兩個效果:

屏幕快照 2017-03-30 下午4.52.32.png
屏幕快照 2017-03-30 下午4.52.42.png
  • 導航欄的透明度隨著UITableView的滑動距離而變化(這個功能其實也可以使用UIScrollViewDelegate的代理方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView來實現(xiàn))。
    (1) 為UITableView的contentOffset屬性注冊觀察浓冒,其中context這個參數(shù)是為了區(qū)分父類是否對該消息感興趣栽渴。
    [self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:KVOContext_ContentOffset];

(2)實現(xiàn)觀察者回調(diào)方法- (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"];
}

說明:導航欄沒有設(shè)置透明度的方法和屬性,需要先設(shè)置導航欄的背景圖和陰影圖為空的圖片來設(shè)置導航欄的透明裆蒸,接著向?qū)Ш綑诓迦胍粋€視圖熔萧,通過控制該視圖的透明度來達到導航欄的控制導航欄的透明度效果。

    [self.navigationController.navigationBar setBackgroundImage:[[UIImage alloc] init] forBarMetrics:UIBarMetricsDefault];
    [self.navigationController.navigationBar setShadowImage:[[UIImage alloc] init]];
  • 為UITableView添加多個cell僚祷,每個cell上都有一個時間不一樣的倒計時佛致。
    (1)先包裝一個時間數(shù)據(jù)模型
#import <Foundation/Foundation.h>

@interface TimeModel : NSObject

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

@end

(2)在cell中增加一個TimeModel屬性,并在設(shè)置屬性的時候辙谜,注冊觀察俺榆,實現(xiàn)觀察者回調(diào)方法

- (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中設(shè)置時間模型的時候,因為cell是重用的装哆,如果cell中存在時間模型罐脊,得先移除掉對該時間模型的觀察定嗓,再為新賦值的時間模型注冊觀察。
<2>只要設(shè)置了觀察者萍桌,就必須移除宵溅,在觀察者銷毀的時候移除掉所有觀察。

KVO的實現(xiàn)原理

當觀察某對象A時上炎,KVO機制動態(tài)創(chuàng)建一個對象A當前類的子類恃逻,并為這個新的子類重寫了被觀察屬性keyPath的setter 方法。setter 方法隨后負責通知觀察對象屬性的改變狀況藕施。

Apple 使用了 isa 混寫(isa-swizzling)來實現(xiàn) KVO 寇损。當觀察對象A時,KVO機制動態(tài)創(chuàng)建一個新的名為: NSKVONotifying_A的新類裳食,該類繼承自對象A的本類矛市,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會負責在調(diào)用原 setter 方法之前和之后诲祸,通知所有觀察對象屬性值的更改情況浊吏。

在這個過程,被觀察對象的 isa 指針從指向原來的A類烦绳,被KVO機制修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_A類卿捎,來實現(xiàn)當前類屬性值改變的監(jiān)聽;

所以當我們從應(yīng)用層面上看來径密,完全沒有意識到有新的類出現(xiàn)午阵,這是系統(tǒng)“隱瞞”了對KVO的底層實現(xiàn)過程,讓我們誤以為還是原來的類享扔。但是此時如果我們創(chuàng)建一個新的名為“NSKVONotifying_A”的類()底桂,就會發(fā)現(xiàn)系統(tǒng)運行到注冊KVO的那段代碼時程序就崩潰,因為系統(tǒng)在注冊監(jiān)聽的時候動態(tài)創(chuàng)建了名為NSKVONotifying_A的中間類惧眠,并指向這個中間類了籽懦。

(isa 指針的作用:每個對象都有isa 指針,指向該對象的類氛魁,它告訴 Runtime 系統(tǒng)這個對象的類是什么暮顺。所以對象注冊為觀察者時,isa指針指向新子類秀存,那么這個被觀察的對象就神奇地變成新子類的對象(或?qū)嵗┝舜仿搿#?因而在該對象上對 setter 的調(diào)用就會調(diào)用已重寫的 setter,從而激活鍵值通知機制或链。

KVO的鍵值觀察通知依賴于 NSObject 的兩個方法:willChangeValueForKey:和 didChangevlueForKey:惫恼,在存取數(shù)值的前后分別調(diào)用2個方法:

被觀察屬性發(fā)生改變之前,willChangeValueForKey:被調(diào)用澳盐,通知系統(tǒng)該 keyPath 的屬性值即將變更祈纯;當改變發(fā)生后令宿, didChangeValueForKey: 被調(diào)用,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更腕窥;之后袁翁, observeValueForKey:ofObject:change:context: 也會被調(diào)用吊档。且重寫觀察屬性的setter 方法這種繼承方式的注入是在運行時而不是編譯時實現(xiàn)的却邓。

KVO的自我實現(xiàn)

  • 創(chuàng)建一個類毕荐,用于存儲觀察者,觀察的屬性冕碟,以及觀察者的回調(diào)方法。
@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
  • 創(chuàng)建一個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
  • 實現(xiàn)添加觀察者的方法- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block
- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block
{
    //1.檢查對象的類有沒有相應(yīng)的 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;
}
  • 實現(xiàn)幾個輔助方法
/**
 根據(jù)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;
}

/**
 根據(jù)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);
    }
}

/**
 根據(jù)類名創(chuàng)建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方法失斕6!");
    }
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
}
  • 實現(xiàn)移除觀察者的方法
- (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];
    }
}

說明:在這里我只實現(xiàn)了對CGPoint屬性的刻伊,如果真的要實現(xiàn)一個KVO框架的話露戒,還需定義一個數(shù)據(jù)模型出來,用來承載任何類型的屬性捶箱,并能夠解析出對應(yīng)的數(shù)據(jù)智什。

使用自定義的KVO

使用上面的第一個示例,對UITableView的contentOffset屬性進行監(jiān)聽讼呢,實現(xiàn)對導航欄透明度的控制

[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"];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撩鹿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悦屏,更是在濱河造成了極大的恐慌节沦,老刑警劉巖键思,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異甫贯,居然都是意外死亡吼鳞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門叫搁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赔桌,“玉大人,你說我怎么就攤上這事渴逻〖驳常” “怎么了?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵惨奕,是天一觀的道長雪位。 經(jīng)常有香客問我,道長梨撞,這世上最難降的妖魔是什么雹洗? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮卧波,結(jié)果婚禮上时肿,老公的妹妹穿的比我還像新娘。我一直安慰自己港粱,他們只是感情好螃成,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著查坪,像睡著了一般锈颗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咪惠,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天击吱,我揣著相機與錄音,去河邊找鬼遥昧。 笑死覆醇,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的炭臭。 我是一名探鬼主播永脓,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鞋仍!你這毒婦竟也來了常摧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎落午,沒想到半個月后谎懦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡溃斋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年界拦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梗劫。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡享甸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出梳侨,到底是詐尸還是另有隱情蛉威,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布走哺,位于F島的核電站瓷翻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏割坠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一妒牙、第九天 我趴在偏房一處隱蔽的房頂上張望彼哼。 院中可真熱鬧,春花似錦湘今、人聲如沸敢朱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拴签。三九已至,卻和暖如春旗们,著一層夾襖步出監(jiān)牢的瞬間蚓哩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工上渴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留岸梨,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓稠氮,卻偏偏與公主長得像曹阔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子隔披,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 本文分為2個部分:概念與應(yīng)用赃份。概念部分旨在剖析 KVO 這一設(shè)計模式的實現(xiàn)原理;應(yīng)用部分通過創(chuàng)建的項目奢米,以說明 K...
    啊左閱讀 57,637評論 107 438
  • iOS--KVO的實現(xiàn)原理與具體應(yīng)用 長時間不用容易忘,這篇文章挺好的.轉(zhuǎn)載自看本文分為2個部分:概念與應(yīng)用抓韩。概念...
    超_iOS閱讀 1,436評論 0 17
  • 一纠永、概述 KVO,即:Key-Value Observing园蝠,它提供一種機制渺蒿,當指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,046評論 11 33
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉彪薛,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,690評論 0 9
  • 上半年有段時間做了一個項目茂装,項目中聊天界面用到了音頻播放,涉及到進度條善延,當時做android時候處理的不太好少态,由于...
    DaZenD閱讀 3,014評論 0 26