上篇文章講到了什么是isa指針以及KVO的底層實(shí)現(xiàn),如果對KVO和isa指針不熟悉的需要先看看這篇文章。本篇文章主要是實(shí)現(xiàn)含有Block的KVO方法。先上代碼
1、 KVO的簡單實(shí)現(xiàn)
從上篇文章中我們知道KVO的底層是通過運(yùn)行時動態(tài)創(chuàng)建一個子類進(jìn)行監(jiān)聽屬性的變化的。我們這里先給出一個簡單的實(shí)現(xiàn):
1、創(chuàng)建一個Dog類,含有有個name屬性。
2、手動創(chuàng)建一個SimpleKVO_Dog類繼承自Dog類,重寫setName方法。
3、給NSObject添加個category,增加添加觀察者的方法和觀察者回調(diào)方法,實(shí)現(xiàn)代碼如下:
NSObject的category中的代碼:
#import "SimpleKVO_Dog.h"
NSString *const ObserverKey = @"ObserverKey";
@implementation NSObject (SimpleKVO)
- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
// 保存觀察者
objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 修改isa指針指向的類(指向了Dog子類),這里將指向我們手動創(chuàng)建的Dog的子類
object_setClass(self, [SimpleKVO_Dog class]);
}
// 這里做是為了容錯處理
- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{}
@end
子類重寫的setName方法實(shí)現(xiàn):
- (void)setName:(NSString *)name{
// 保存舊值
NSString *oldName = self.name;
// 調(diào)用父類方法
[super setName:name];
// 獲取觀察者
id obsetver = objc_getAssociatedObject(self, ObserverKey);
NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
// 調(diào)用回調(diào)方法,傳遞舊值和新值
[obsetver ll_observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}
調(diào)用代碼:
- (void)test{
Dog *dog = [Dog new];
//isa --->Dog類
[dog ll_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
//isa---> SimpleKVO_Dog類
dog.name = @"aaa";
dog.name = @"bbb";
}
- (void)ll_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
打印結(jié)果:
上述方法中我們是通過手動創(chuàng)建Dog的子類SimpleKVO_Dog類,并重寫了父類的setName
方法,通過修改Dog類實(shí)例isa指針的指向,來調(diào)用子類的setName方法。子類的setName方法中又調(diào)用父類的setName方法以及通知了觀察者屬性改變。
2、帶有block的實(shí)現(xiàn)
下面我們來通過runtime
動態(tài)生成子類,并實(shí)現(xiàn)帶有block回調(diào)的方法,我們?nèi)砸訢og類為例。下面我們通過代碼進(jìn)一步去講解:
首先先給出兩個工具方法(getter方法名和setter方法名的相互轉(zhuǎn)化):
//根據(jù)getter方法名返回setter方法名 name -> Name -> setName:
- (NSString *)setterForGetter:(NSString *)key
{
// 1. 首字母轉(zhuǎn)換成大寫
unichar c = [key characterAtIndex:0];
NSString *str = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c-32]];
// 2. 最前增加set, 最后增加:
NSString *setter = [NSString stringWithFormat:@"set%@:", str];
return setter;
}
//根據(jù)setter方法名返回getter方法名 setName: -> Name -> name
- (NSString *)getterForSetter:(NSString *)key
{
// 1. 去掉set
NSRange range = [key rangeOfString:@"set"];
NSString *subStr1 = [key substringFromIndex:range.location + range.length];
// 2. 首字母轉(zhuǎn)換成大寫
unichar c = [subStr1 characterAtIndex:0];
NSString *subStr2 = [subStr1 stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c+32]];
// 3. 去掉最后的:
NSRange range2 = [subStr2 rangeOfString:@":"];
NSString *getter = [subStr2 substringToIndex:range2.location];
return getter;
}
下面我們來看具體邏輯實(shí)現(xiàn):
添加觀察者:ll_addObserver:key:callback:
步驟:
- 檢查被觀察對象對應(yīng)的類有沒有相應(yīng)的 setter 方法,沒有則return;
- 檢查對象 isa 指向的類是不是一個 KVO 類。如果不是,新建一個繼承原來類的子類,并把 isa 指向這個新建的子類;(這里還需要判斷需要新建的子類是否已經(jīng)創(chuàng)建過了,如果創(chuàng)建過了,則直接使用該子類);
- 檢查對象的 KVO 類重寫過沒有這個 setter 方法。如果沒有,添加重寫的 setter 方法;
- 將觀察者、觀察的key以及對應(yīng)的block回調(diào)生成相應(yīng)的字典保存到數(shù)組里;
-(void)ll_addObserver:(id)observer key:(NSString *)key callback:(LLKVOBlock)callback{
//1. 通過觀察的key獲得相應(yīng)的setter方法
SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) return; //不存在setter方法直接return
//2. 檢查對象 isa 指向的類是不是一個 KVO 類。如果不是,新建一個繼承原來類的子類,并把 isa 指向這個新建的子類
Class clazz = object_getClass(self);
NSString *className = NSStringFromClass(clazz);
if (![className hasPrefix:KVOPrefix]) {//當(dāng)前類不是KVO類
clazz = [self ll_KVOClassWithOriginalClassName:className];
object_setClass(self, clazz);
}
//-------到這里self已經(jīng)是KVO類了---------
// 3. 檢查KVO類是否已重寫父類的setter方法,如果沒有則為KVO類添加setter方法的實(shí)現(xiàn)
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
};
// 4. 添加該觀察者到觀察者列表中
// 4.1 創(chuàng)建觀察者相關(guān)信息字典(觀察者對象、觀察的key、block)
NSDictionary *infoDic = @{@"observer":observer,@"key":key,@"callback":callback};
// 4.2 獲取關(guān)聯(lián)對象(裝著所有觀察者的數(shù)組)
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, ObserverArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:infoDic];
}
創(chuàng)建子類的步驟:
- 判斷要創(chuàng)建的子類是否已經(jīng)創(chuàng)建,存在直接返回這個類,不存在則去創(chuàng)建;
-
重寫子類的class方法,使其返回父類的Class;
(這步只是模仿系統(tǒng)KVO的實(shí)現(xiàn),對業(yè)務(wù)邏輯沒影響,可不實(shí)現(xiàn))
// 動態(tài)創(chuàng)建子類的方法
-(Class)ll_KVOClassWithOriginalClassName:(NSString *)className
{
NSString *kvoClassName = [KVOPrefix stringByAppendingString:className];
Class kvoClass = NSClassFromString(kvoClassName);//如果類不存在這個方法返回的值為nil
// 如果kvo class存在則返回
if (kvoClass) {
return kvoClass;
}
// 如果kvo class不存在, 則創(chuàng)建這個類
Class originClass = object_getClass(self);
kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
// 修改kvo class方法的實(shí)現(xiàn)
Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClass, @selector(class), (IMP)ll_class, types);
// 注冊kvo_class
objc_registerClassPair(kvoClass);
return kvoClass;
}
// 重寫的class方法的IMP
static Class ll_class(id self, SEL cmd)
{
//模仿Apple的做法, 欺騙人們這個kvo類還是原類
return class_getSuperclass(object_getClass(self));
}
下面我們來看一下子類的setter方法的實(shí)現(xiàn),這和簡單實(shí)現(xiàn)的思路是一樣的,同樣是:
- 獲取原來的值;
- 調(diào)用父類的setter方法;
- 通知觀察者屬性改變了(這里換成了block);
static void kvo_setter(id self, SEL _cmd, id newValue)
{
// 1. 獲取舊值
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [self getterForSetter:setterName];
id oldValue = [self valueForKey:getterName];
// 2. 調(diào)用父類方法
struct objc_super superClazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
objc_msgSendSuper(&superClazz, _cmd, newValue);
// 3、獲取觀察者列表,遍歷找出對應(yīng)的觀察者,執(zhí)行響應(yīng)的block
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
for (NSDictionary *info in observers) {
if ([info[@"key"] isEqualToString:getterName]) {
// gcd異步調(diào)用callback
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, newValue);
});
}
}
}
注:這里調(diào)用父類的方法通過id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
實(shí)現(xiàn),第一個參數(shù)是一個指向objc_super結(jié)構(gòu)體的指針, objc_super的定義如下
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
objc_super結(jié)構(gòu)體包含兩個成員,receiver表示某個類的實(shí)例,這里為self,super_class表示當(dāng)前類的父類,這里為self的父類。(注:這里的self其實(shí)已經(jīng)是KVO創(chuàng)建的子類類型了)我們這里通過class_getSuperclass(object_getClass(self))
方法獲得;
到這里添加觀察者的方法暫時差不多了,為什么說暫時呢因為還有些問題,在下面會提出。那么現(xiàn)在我們還需要添加移除觀察者的方法:
-(void)ll_removeObserver:(id)observer key:(NSString *)key
{
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
if (!observers) return;
for (NSDictionary *info in observers) {
if([info[@"key"] isEqualToString:key]) {
[observers removeObject:info];
break;
}
}
// 如果觀察者列表count為0,則修改kvo類的isa指針,指向原來的類
if (observers.count == 0) {
Class clazz = object_getClass(self);
NSString *className = NSStringFromClass(clazz);
Class oriClass =NSClassFromString([className substringFromIndex:KVOPrefix.length]);
object_setClass(self, oriClass);
}
}
值得注意的是,當(dāng)所有觀察者都移除后,修改isa指針使其指向原來的類。系統(tǒng)的KVO實(shí)現(xiàn)就是這么做的,我們可以簡單的通過代碼測試一下:
-(void)test1{
self.dog = [Dog new];
NSLog(@"%@",object_getClass(self.dog));
[self.dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@",object_getClass(self.dog));
[self.dog removeObserver:self forKeyPath:@"age"];
NSLog(@"%@",object_getClass(self.dog));
}
輸出結(jié)果:Dog----NSKVONotifying_Dog----Dog
3、 存在的問題:
因為重寫setter方法我們的實(shí)現(xiàn)static void kvo_setter(id self, SEL _cmd, id newValue){}
是 這樣的,newValue是一個id類型,這就要求我們觀察的屬性必須是OC類的實(shí)例。通過嘗試發(fā)現(xiàn)系統(tǒng)的KVO會將基本類型最終轉(zhuǎn)換成NSNumber類型,再將新/舊值通過字典傳遞。但是OC對象我們可以通過id來統(tǒng)一表示,基本類型我們卻無能為力。所以這里給出兩種思路:
-
思路一:可以在添加觀察者方法中的第3步
給kvo類重寫setter方法
,我們通過判斷參數(shù)類型,來添加不同setter的方法實(shí)現(xiàn)。類型的判斷這里用到了@encode關(guān)鍵字,不明白的可以看這篇文章
// 3. 檢查KVO類是否已重寫父類的setter方法,如果沒有則為KVO類添加setter方法的實(shí)現(xiàn)
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
// 獲取參數(shù)類型
char *type = method_copyArgumentType(setterMethod, 2);
if (strcmp(type, "@") == 0) {//對象類型
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}else if (strcmp(type, @encode(long)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)long_setter, types);
}else if (strcmp(type, @encode(int)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)int_setter, types);
}else if (strcmp(type, @encode(float)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
}else if (strcmp(type, @encode(double)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
}else if (strcmp(type, @encode(BOOL)) == 0) {
class_addMethod(clazz, setterSelector, (IMP)bool_setter, types);
}
};
但是這種思路的問題就是需要判斷的類型太多,除對象類型外都需要實(shí)現(xiàn)不同的setter方法的IMP,而且代碼內(nèi)容大致相同,造成代碼的重復(fù)。這里給出int類型的setter的方法IMP:
//int 類型
static void int_setter(id self, SEL _cmd, int newValue)
{
// 1. 檢查getter方法是否存在
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [self getterForSetter:setterName];
if (!getterName) {
return;
}
// 2. 獲取舊值
id oldValue = [self valueForKey:getterName];
// 3. 調(diào)用父類方法
struct objc_super superClazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
objc_msgSendSuper(&superClazz, _cmd, newValue);
// 4、獲取觀察者列表,遍歷找出對應(yīng)的觀察者,執(zhí)行響應(yīng)的block
NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
for (NSDictionary *info in observers) {
if ([info[@"key"] isEqualToString:getterName]) {
// gcd異步調(diào)用callback
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, [NSNumber numberWithInt:newValue]);
});
}
}
}
-
思路二:我們是否可以模仿系統(tǒng)KVC的實(shí)現(xiàn)通過
[dog setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
這樣的形式,在給子類添加setter方法前,通過轉(zhuǎn)換成NSNumer類型后,在實(shí)現(xiàn)setter方法呢。然并卵,我也沒能實(shí)現(xiàn)。需請大神支援~~~
到這里本篇文章基本結(jié)束,文中所涉及代碼都在這里
最后:喜歡我文章的可以多多點(diǎn)贊和關(guān)注,您的鼓勵是我寫作的動力。O(∩_∩)O~