在上一篇博客中,說明了 KVO 的執(zhí)行過程和基本的實現(xiàn)原理。
KVO的執(zhí)行原理
- 對象本身作為事件的發(fā)布者,在自己被觀察者(通常是那個包含自己的控制器)觀察的屬性發(fā)生改變的時候,向觀察者發(fā)布屬性修改了的通知。(本質(zhì)就是在 setter 方法調(diào)用的時候執(zhí)行發(fā)布)
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
- 控制器,本身之前實現(xiàn)訂閱好這個 setter 通知,并在事件響應(yīng)函數(shù)中,對這個的事件發(fā)布做出相應(yīng)。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
這些道理都懂,但如何自己在了解了 KVO 實現(xiàn)原理之后,自己實現(xiàn)一個 KVO 呢?
為什么要自己實現(xiàn) KVO?
- 熟悉 runtime 的各種使用方法。
- 傳統(tǒng)的 KVO,所有的屬性訂閱都幾種在了一個方法相應(yīng)體里面??雌饋聿惶珒?yōu)雅(裝逼)
if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
} else if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
} else if ([object isKindOfClass:[xxx class]] && [keyPath isEqualToString:@"xxx"]) {
} ......
實現(xiàn)目標(biāo)
希望能夠以 block 回調(diào)的方式來自己實現(xiàn) KVO。
好處在于,每一個屬性對應(yīng)一個自己的回調(diào) block。
這樣代碼看起來比較清晰和緊湊。
也符合函數(shù)是編程的思想。
最終需要實現(xiàn)的代碼效果
[self.person rl_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
NSLog(@"observer : %@ keyPath : %@ oldValue : %@ newValue : %@",observer,keyPath,oldValue,newValue);
}];
[self.person rl_addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
//... 這是 age 的改變時的 KVO block 回調(diào)
}];
.....
在開始先自己的目標(biāo)方法之前,首先在回顧一下 KVO 的實現(xiàn)基本步驟
- 創(chuàng)建一個當(dāng)前類的子類。
- 在子類中重寫當(dāng)前屬性的 setter 方法。
- 在子類重寫的 setter 方法里面,手動的調(diào)用 KVO 實現(xiàn)的兩句代碼。

當(dāng)然,和真實的 KVO 相比,這里缺少了 _isKVO , class , dealloc 的實現(xiàn)。
首要目的,先是以函數(shù)式編程的方法,來實現(xiàn)自己的 KVO。
開始動手來實現(xiàn)自己的 KVO。
第一步:添加一個 NSObject 的分類
typedef void(^KVOBlock)(id observer,NSString *keyPath,id oldValue ,id newValue);
@interface NSObject (RLKVO)
- (void)rl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context withBlock:(KVOBlock)block;
@end
方法名哪來的?
復(fù)制粘貼系統(tǒng)自帶的 KVO 的方法。
后面加入了一個 block用來當(dāng)屬性發(fā)生改變時,回調(diào)觀察者。
第二步:實現(xiàn)這個方法
先考慮一下,在這個方法內(nèi)部,需要做什么事情。
- keyPath 的有效性檢查。
- 根據(jù)當(dāng)前 self,創(chuàng)建一個子類。
- 當(dāng)類創(chuàng)建完畢之后,修改當(dāng)前 self 的 isa 指針到這個子類。
- 在子類中,添加一個 set方法,用來重寫父類的 setter 方法。(用 class_addMethod 添加方法 sel 和 真實的函數(shù)名,不用匹配,只要建立雙方的聯(lián)系就行了。此例子中,sel = setName: 而真實的 C 函數(shù)叫 kvoSetter)。sel & IMP 不需要匹配名字
- 在子類的 setter 方法內(nèi)部,調(diào)用 self willChangeValueForKey: & self didChangeValueForkey:。
當(dāng)然,還有有一些細(xì)枝末節(jié)的東西。在代碼注釋里會有說明。
- (void)rl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context withBlock:(KVOBlock)block {
// keyPath 的檢查。
NSString *setterName = setterFormat(keyPath);
SEL sel = NSSelectorFromString(setterName);
// 這里獲取 method 的意義在于,可以方便的獲取當(dāng)前方法的 EncodingType 一遍在添加方法class_addMethod的時候,可以設(shè)置 EncodingType.
Method setterMethod = class_getInstanceMethod([self class], sel);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%@ have not %@ method",[self class],setterName] userInfo:nil];
}
// 獲取前類的類名,并作為之后創(chuàng)建子類的父類。
NSString *superClassName = NSStringFromClass([self class]);
const char *type = method_getTypeEncoding(setterMethod); // 通過方法結(jié)果提,拿到方法編碼。EncodingType。否則就需要自己手拼寫 v@:@ 麻煩。
// 動態(tài)創(chuàng)建類
Class newClass = [self createClassFromSuperName:superClassName sel:sel encodingType:type];
// 替換當(dāng)前 self 的 isa 指針
object_setClass(self, newClass);
// 保存信息
RLKVO_Info *info = [[RLKVO_Info alloc] initWithObserver:observer block:block keypath:keyPath];
NSMutableArray *infoArray = objc_getAssociatedObject(self, &infoArrayPro);
if (!infoArray) {
infoArray = [NSMutableArray array];
objc_setAssociatedObject(self, &infoArrayPro, infoArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[infoArray addObject:info];
}
此段代碼解釋:
- 首先根據(jù)用戶傳入的 keyPath 字符串,配置成 setKeyPath: 字符串,表示 setter 方法的sel。
- 獲取這個 setKeyPath: 的 sel
- 根據(jù) sel 獲取當(dāng)期對應(yīng)的方法 Method
- 如果 Method 不存在,說明傳入的 keyPath 是錯誤的。對象沒有這個屬性,拋出異常。
- 獲取當(dāng)前對象的 className,以備創(chuàng)建子類的時候使用。
- 根據(jù) Method 拿到方法的 EncodingType 簽名。
- 動態(tài)的創(chuàng)建子類。
創(chuàng)建當(dāng)前類的子類方法
/**
根據(jù)父類創(chuàng)建子類
@param superName 父類類型字符串
@param sel 父類當(dāng)前方法的 sel
@param encodingType 父類當(dāng)前方法的 EncodingType
@return 返回以當(dāng)前父類創(chuàng)建的新類。
*/
- (Class)createClassFromSuperName:(NSString *)superName sel:(SEL)sel encodingType:(const char *)encodingType {
// 創(chuàng)建一個類.
Class newClass = objc_allocateClassPair(
NSClassFromString(superName), // 當(dāng)前類的基類
[NSString stringWithFormat:@"RLKVO_%@",
NSStringFromClass([self class])].UTF8String,// 類名
0);
// const char *types = method_getTypeEncoding(class_getInstanceMethod(NSClassFromString(superName), @selector(class)));
// 往新類中添加方法
class_addMethod(newClass, sel, (IMP)kvoSetter, encodingType);
// 類創(chuàng)建完畢之后,注冊到 runtime
objc_registerClassPair(newClass);
// 返回這個新類。
return newClass;
}
此段代碼說明:
- 動態(tài)的創(chuàng)建一個子類,類名為 RLKVO_父類的名字。
- 往新類中,添加一個 kvoSetter 的方法,方法簽名使用父類的 setKeyPath: 。
- 新類創(chuàng)建完畢,還并沒有完成,需要把新類注冊到 runtime。
- 最后返回已經(jīng)注入到 runtime 的這個子類。
當(dāng)子類創(chuàng)建,完畢之后,就需要把當(dāng)前對象的 isa 指針改成新創(chuàng)建的這個類了。
也就是第一段代碼的。
// 動態(tài)創(chuàng)建類
Class newClass = [self createClassFromSuperName:superClassName sel:sel encodingType:type];
// 替換當(dāng)前 self 的 isa 指針
object_setClass(self, newClass);
到目前為止,做的事情,主要包括創(chuàng)建了一個基于當(dāng)前類的子類 & 把當(dāng)前類的 isa 指針改成了子類。
往子類中添加的 kvoSetter 方法具體實現(xiàn)
// 使用 class_addMethod runtime 添加方法。
void kvoSetter(id self,SEL _cmd,id newValue) {
// 拿到 setter
NSString *setterName = NSStringFromSelector(_cmd);
// 根據(jù) setter 拿到 getter
NSString *getterName = getterFormat(setterName);
id oldValue = [self valueForKey:getterName];
if (!getterName) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"getter 方法不存在" userInfo: nil];
}
// 手動開啟 KVO
[self willChangeValueForKey:getterName];
// 調(diào)用父類的方法。
// 定義一個函數(shù)指針
void(*objc_msgSendRLKVO)(void *,SEL ,id) = (void *)objc_msgSendSuper;
struct objc_super superClassStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
objc_msgSendRLKVO(&superClassStruct,_cmd,newValue);
[self didChangeValueForKey:getterName];
//
NSMutableArray *infoArrM = objc_getAssociatedObject(self, &infoArrayPro);
if (infoArrM) {
[infoArrM enumerateObjectsUsingBlock:^(RLKVO_Info *info, NSUInteger idx, BOOL * _Nonnull stop) {
info.block(info.observer, info.keyPath, oldValue, newValue);
}];
}
}
在方法實現(xiàn)中,最重要的就是需要調(diào)用 block,把 KVO 的相關(guān)數(shù)據(jù)回調(diào)出去給觀察者。
NSMutableArray *infoArrM = objc_getAssociatedObject(self, &infoArrayPro);
if (infoArrM) {
[infoArrM enumerateObjectsUsingBlock:^(RLKVO_Info *info, NSUInteger idx, BOOL * _Nonnull stop) {
info.block(info.observer, info.keyPath, oldValue, newValue);
}];
}
最后運行結(jié)果:
- (void)viewDidLoad {
[super viewDidLoad];
_person = [RLPerson new];
_person.name = @"李四";
[_person rl_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil withBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
NSLog(@"keyPath : %@ oldValue: %@ newValue : %@",keyPath,oldValue,newValue);
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_person.name = [NSString stringWithFormat:@"%@-",_person.name];
}

此次實現(xiàn)過程中,代碼細(xì)節(jié)有很多,光使用文字很難說的清楚。
我把源碼放到了 github 上面。有興趣的小伙伴可以下載來看看。
注意:此 demo 應(yīng)該不能使用到實際的開發(fā)環(huán)境中。還有很多細(xì)節(jié)沒有處理。比如 _iskVO class dealloc 等。僅供學(xué)習(xí)之用。