一、KVO 簡介
KVO(Key-Value Observing)是iOS提供的一種監(jiān)聽屬性變化的機制缭召。
二重斑、使用場景
基本使用:
- 添加觀察者
任意定義一個包含了屬性的類:
@interface KVO : NSObject
@property (nonatomic, assign) NSUInteger count;
@property (nonatomic, copy) NSString *name;
@end
添加一個對上述類實例對象的屬性值監(jiān)聽者:
KVO *kvoObj = [KVO new];
[kvoObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
上述代碼對kvoObj的name屬性進(jìn)行了監(jiān)聽银酗,其中監(jiān)聽的策略是NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld堰酿,表示屬性值改變時通知內(nèi)容里包含新的值和老的值;
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01, //指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變后的新值昭卓。
NSKeyValueObservingOptionOld = 0x02, //指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變前的舊值愤钾。
NSKeyValueObservingOptionInitial = 0x04, //當(dāng)指定了這個選項時,在addObserver:forKeyPath:options:context:消息被發(fā)出去后候醒,甚至不用等待這個消息返回能颁,監(jiān)聽者對象會馬上收到一個通知
NSKeyValueObservingOptionPrior = 0x08 //當(dāng)指定了這個選項時,在被監(jiān)聽的屬性被改變前火焰,監(jiān)聽者對象就會收到一個通知
}
另外劲装,context這里直接用nil,其實這個參數(shù)可以用來傳值或者使用靜態(tài)變量來標(biāo)志一個指定的通知昌简。
- 添加觀察者通知響應(yīng)函數(shù)
需要重寫非正式協(xié)議NSKeyValueObserving的下述方法以接收屬性值改變時發(fā)出的通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
這里只簡單的打印出change里的信息;
- 屬性改變時通知觀察者
kvoObj.name = @"newName";
運行結(jié)果:
打印出了change字典對象里的內(nèi)容绒怨,new和old分別對應(yīng)NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld選項纯赎;
其中,kind = 1表示改變的類型為設(shè)置類型南蹂,具體代碼對應(yīng)的類型如下:
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2, //針對集合類型屬性犬金,表示插入元素到該集合對象
NSKeyValueChangeRemoval = 3, //同上,表示移除元素從該集合對象
NSKeyValueChangeReplacement = 4 //同上六剥,表示從該集合對象屬相總替換元素
};
typedef NSUInteger NSKeyValueChange;
需要注意晚顷,添加監(jiān)聽的方法addObserver:forKeyPath:options:context:并不會對監(jiān)聽和被監(jiān)聽的對象以及context做強引用,你必須自己保證他們在監(jiān)聽過程中不被釋放疗疟。
- 移除觀察者
當(dāng)不再需要監(jiān)聽該屬性的時候该默,或者觀察者需要被釋放前,需要從被觀察者隊列中移除策彤,否則被觀察者繼續(xù)發(fā)送通知則會導(dǎo)致野指針程序崩潰栓袖,具體實現(xiàn)如下:
- (void)dealloc {
[kvoObj removeObserver:self forKeyPath:@"name"];
}
三匣摘、自己實現(xiàn)KVO
蘋果官方文檔:
Automatic key-value observing is implemented using a technique called
isa-swizzling...When an observer is registered for an attribute of an
object the isa pointer of the observed object is modified, pointing to
an intermediate class rather than at the true class. As a result the
value of the isa pointer does not necessarily reflect the actual class
of the instance.
大概意思就是說利用了isa_swizzling技術(shù),大家都知道swizzling是一種OC級別的Hook技術(shù)裹刮,所以isa_swizzling就是一種isa Hook技術(shù)音榜,在一個支持KVO的對象被添加了觀察者,系統(tǒng)會為其生成一個子類捧弃,重寫了setXXX方法(XXX為被監(jiān)聽的屬性名)赠叼,并將該實例的Isa指針指向了新的這個子類(class),這樣對被觀察者進(jìn)行屬性賦值的時候調(diào)用的是重寫后的setXXX方法,而setXXX方法內(nèi)部添加了通知機制违霞;
那么我們自己手動來實現(xiàn)一個簡單的KVO:
- 3.1 自己實現(xiàn)一個添加觀察者的方法:
//
// SUKVO.h
// DreamOneByOne
//
// Created by He on 2017/7/16.
// Copyright ? 2017年 Sevenuncle. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol SUKVODelegate <NSObject>
-(void)su_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
@end
@interface SUKVO : NSObject
@property (copy, nonatomic) NSString *desc;
- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
模仿NSKeyValueObserving協(xié)議(分類)定義添加觀察者的方法以及一個代理用于接收通知嘴办。下面是其內(nèi)部實現(xiàn):
// SUKVO.m
#import "SUKVO.h"
#import "SUKVO_SubClass.h"
#import <objc/message.h>
@implementation SUKVO
- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context {
//動態(tài)添加觀察者對象
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//動態(tài)改變被觀察者isa指針,使訪問到改變后的setXXX方法
object_setClass(self, [SUKVO_SubClass class]);
}
@end
當(dāng)添加入一個觀察者時葛家,利用了runtime動態(tài)添加屬性接口將該觀察者加入該被觀察實例對象上(實際上需要維護(hù)一個隊列户辞,用于記錄所有觀察該屬性的觀察者,這里為了簡單)癞谒,用于后續(xù)監(jiān)聽屬性改變時給這個觀察者發(fā)送消息底燎;
然后,就是同樣利用runtime改變該被觀察者實例對象的isa指針(class)弹砚,這樣后續(xù)發(fā)送消息給被觀察者均是往SUKVO_SubClass定義的方法里找(系統(tǒng)實現(xiàn)的子類為NSKVONotifying_原有類)双仍;
- 3.2 接下來看一下SUKVO_SubClass子類的實現(xiàn):
#import "SUKVO_SubClass.h"
#import <objc/message.h>
@implementation SUKVO_SubClass
- (void)setDesc:(NSString *)desc {
[self willChangeValueForKey:@"desc"]; //改變之前通知
[super setDesc:desc];
[self didChangeValueForKey:@"desc"]; //改變之后通知
//通知值改變,這里為了圖方便桌吃,簡單的直接發(fā)送改變通知朱沃,實際上系統(tǒng)的實現(xiàn)利用了消息通知機制
id observer = objc_getAssociatedObject(self, "observer");
if([observer respondsToSelector:@selector(su_observeValueForKeyPath:ofObject:change:context:)]) {
[observer su_observeValueForKeyPath:@"desc" ofObject:self change:nil context:nil];
}
}
@end
這樣就實現(xiàn)了簡單的KVO,不過系統(tǒng)為了滿足可以添加多個觀察者監(jiān)聽同一個屬性的需求茅诱,不能像上述實現(xiàn)的這么簡單逗物,需要一個一個字典加隊列,這樣每一個屬性對應(yīng)一個觀察者隊列瑟俭,然后由內(nèi)部一個通知中心統(tǒng)一給觀察者發(fā)送通知翎卓;
四、與 KVC 的關(guān)系
關(guān)于KVO和KVC之間是否有聯(lián)系摆寄,在網(wǎng)絡(luò)上搜索了一通失暴,也沒個定論,不過大眾普遍認(rèn)為KVO和KVC通常是有聯(lián)系的微饥;但是逗扒,當(dāng)了解了KVO的實現(xiàn)機制后,如上面自己實現(xiàn)KVO中欠橘,發(fā)現(xiàn)并未用到KVC矩肩,所以部分人開始懷疑KVO真的是基于KVC實現(xiàn)的嗎?
那么試想一個問題:
對于一個包含只讀(readonly)屬性的變量简软,為什么也能通過setVaule:forKey進(jìn)行賦值蛮拔?因為對于默認(rèn)readonly屬性述暂,系統(tǒng)是不會生成set的屬性賦值方法的?那根據(jù)KVO的原理建炫,是無法進(jìn)行鍵值改變的監(jiān)聽的畦韭?
為了驗證上面這個問題,我們需要確認(rèn)兩個事情:
- 對于一個readonly屬性并且同時對該屬性進(jìn)行了觀察者監(jiān)聽肛跌,是否有setXXX方法艺配?
- 是否能夠使用KVC對readonly屬性賦值?
如果上述不包含setXXX方法并且能夠使用KVC對只讀屬性賦值衍慎,就說明KVC內(nèi)部包含了對KVO的支持转唉!
- 下面開始驗證第一個問題:
下面的KVCObject類包含了一個只讀屬性readonly和一個讀寫的屬性,
@interface KVCObject : NSObject
@property (nonatomic, assign) NSUInteger count;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy) NSString *location;
@end
同時對一個KVCObject實例對象添加了對name屬性進(jìn)行監(jiān)聽的觀察者:
kvcObj = [KVCObject new];
[kvcObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[kvcObj addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
打印出此時kvcObj所在的類包含的實例方法:
KVCObject *tmpObj = kvcObj;
Class currentClass = object_getClass(tmpObj);
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
int i = 0;
for (; i < methodCount; i++) {
NSLog(@"%@ - %@", [NSString stringWithCString:class_getName(currentClass) encoding:NSUTF8StringEncoding], [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding]);
}
if( [tmpObj respondsToSelector:@selector(setName:)]) {
NSLog(@"name");
}
if( [tmpObj respondsToSelector:@selector(setLocation:)]) {
NSLog(@"location");
}
上面的代碼打印出了一個對象添加了監(jiān)聽之后,生成的新的子類包含的實例方法列表:
可以看出稳捆,讀寫屬性NSString * location生成了對應(yīng)的setXXX方法赠法,而只讀屬性name沒有生成對應(yīng)的setXXX方法,所以第一個問題得到驗證乔夯。
- 驗證第二個問題:此時通過KVC改變只讀屬性的值砖织,能夠得到KVO值改變通知?
[kvcObj setValue:@"newName" forKey:@"name"];
監(jiān)聽回調(diào)方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
輸出結(jié)果:
可以看出末荐,只讀屬性name在沒有setName方法的情況下侧纯,通過KVC改變值得方式也得到了KVO通知,所以可以下結(jié)論甲脏,KVC內(nèi)部的實現(xiàn)機制支持了KVO眶熬,KVO是依賴KVC的,并不像部分人懷疑的KVC和KVO之間毫無聯(lián)系块请。