你們都知道kvo是一種設計模式,是一種鍵值觀察,當屬性的值改變時候會觸發回調,獲取該屬性的舊值和新值。可是可能有些朋友不清楚何時用它,使用場景是什麼。當須要監聽一個屬性的值改變時候咱們能夠用到它。好比:git
//註冊kvo
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
//屬性賦值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
count++;
p1.name = [NSString stringWithFormat:@"%d",count];
}
//釋放
- (void)dealloc{
[p1 removeObserver:self forKeyPath:@"name"];
}
複製代碼
//重寫Person類的automaticallyNotifiesObserversForKey返回NO即關閉了自動kvo
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
@end
//註冊kvo
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
//屬性值變化(其實只要該對象的成員變量的值改變便可)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
count++;
[p1 willChangeValueForKey:@"name"];
p1.name = [NSString stringWithFormat:@"%d",count];
[p1 didChangeValueForKey:@"name"];
}
//kvo回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
}
//釋放
- (void)dealloc{
[p1 removeObserver:self forKeyPath:@"name"];
}
複製代碼
系統的kvo是怎麼實現的呢?爲何只要對象的屬性變化了就會觸發回調呢?我也很好奇,在查看資料以前,本身也考慮了一下怎麼實現。
首先系統的kvo任意一個對象均可以調用addObserver方法,能夠肯定應該是NSObject的分類,並新增了這個addObserver的方法。接下來想到的是hook,好比hook Person類對應的setter方法。利用runTime方法交換實如今調用這個setter方法以前獲取該屬性對應的成員變量的值,獲得舊值。以後再調用該setter方法以後再次獲取該屬性對應的成員變量的值,獲得新值。最後再調用observeValueForKeyPath方法把新舊值傳遞給observe對應的類。github
接下來查看資料,Oh,My God 並非本身想的那樣,系統在調用addObserver方法時候動態的建立了一個新的子類繼承該被監聽的對象所對應的類。並重寫了父類的setter方法。並把對象的isa指針從父類指向了該子類。這樣當父類的對象調用setter方法時候就會調用子類的setter方法,在該setter方法內部調用了willChangeValueForKey,didChangeValueForKey方法。以後系統會調用observeValueForKeyPath方法,把舊的和新的值傳遞給oberver所對應的類。在新的子類裏除了重寫了父類的setter方法之外還重寫了class方法,該方法是爲了外界調用class時候隱藏新建立的子類。有一點很奇怪當咱們在addObserver方法後打一個斷點時並把鼠標光標移動到改對象上會發現它竟然顯示的是父類而不是新生成的子類。按理來講咱們把isa指針指向新的子類後該對象應該就屬於子類的實例纔對。設計模式
再以後我來驗證了一下,打印一下isa指針指向的類bash
NSLog(@"p1:%@",object_getClass(p1));
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"p1:%@",object_getClass(p1));
複製代碼
打印結果:app
2019-01-29 14:28:40.983065+0800 KVOCustom[20946:172543] p1:Person
2019-01-29 14:28:40.983401+0800 KVOCustom[20946:172543] p1:NSKVONotifying_Person
複製代碼
發現確實在addOberver方法調用前是指向了Person類,在調用後指向了新類NSKVONotifying_Person,從而證實了addOberver方法內部確實是建立了新類。
以後固然是想了解下這個系統建立的新的類內部實現了哪些方法啦。本身寫了一個打印類內部方法以下:框架
- (void)printMethods:(Class)cls{
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *strM = [NSMutableString string];
[strM appendString:[NSString stringWithFormat:@"%@: ",cls]];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *strMethodName = NSStringFromSelector(method_getName(method));
[strM appendString:strMethodName];
[strM appendString:@", "];
}
NSLog(@"%@",strM);
}
複製代碼
以後咱們調用方法打印函數
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
[p1 printMethods:object_getClass(p1)];
複製代碼
打印結果:ui
2019-01-29 14:35:32.435852+0800 KVOCustom[21016:175547] NSKVONotifying_Person: setName:, class, dealloc, _isKVOA,
複製代碼
說明系統新建立的類內部實現了setName方法(用來重寫父類的setter方法),class方法(爲了隱藏子類),dealloc方法(釋放內存),_isKVOA方法(是系統的kvo方法)url
當咱們探索到這裏基本上也就請楚了系統的kvo的實現。spa
Class classUse(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
複製代碼
int _isKVOAUse(id self,SEL _cmd){
return YES;
}
複製代碼
如今是否是手癢癢想本身手寫個KVO啦,如今咱們清楚了系統KVO的實現,模仿它咱們本身實現一個KVO吧。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath valueChangeBlk:(void(^)(id old, id new))valueChangeBlk{
//建立子類
NSString *oldClass = NSStringFromClass(self.class);
NSString *newClass = [NSString stringWithFormat:@"BSKVONotify_%@",oldClass];
Class classNew = objc_allocateClassPair(self.class, newClass.UTF8String, 16);
objc_registerClassPair(classNew);
object_setClass(self, NSClassFromString(newClass));
//新增set方法
NSMutableString *strM = [NSMutableString string];
[strM appendString:[[keyPath substringToIndex:1] uppercaseString]];
[strM appendString:[keyPath substringFromIndex:1]];
NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM];
class_addMethod(NSClassFromString(newClass), NSSelectorFromString(setMethod), (IMP)keyPathMethod,"v@:@");
//新增class方法
class_addMethod(classNew, NSSelectorFromString(@"class"), (IMP)classUse,"#@:");
//新增_isKVOA方法
class_addMethod(classNew, NSSelectorFromString(@"_isKVOA"),(IMP)_isKVOAUse, "i@:");
//設置關聯對象
objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY);
objc_setAssociatedObject(self, "blk", valueChangeBlk, OBJC_ASSOCIATION_COPY);
objc_setAssociatedObject(self, "classNew", classNew, OBJC_ASSOCIATION_RETAIN);
objc_setAssociatedObject(self, "classOld", self.class, OBJC_ASSOCIATION_RETAIN);
}
複製代碼
void keyPathMethod(id self,IMP _cmd, id arg){
//set方法名,原始類和子類
NSString *keyPath = objc_getAssociatedObject(self, "keyPath");
NSMutableString *strM = [NSMutableString string];
[strM appendString:[[keyPath substringToIndex:1] uppercaseString]];
[strM appendString:[keyPath substringFromIndex:1]];
NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM];
Class subClass = objc_getAssociatedObject(self, "classNew");
Class oldClass = objc_getAssociatedObject(self,"classOld");
//isa指針指向父類,執行set方法
object_setClass(self, oldClass);
//獲取成員變量的值
Ivar ivar = class_getInstanceVariable([self class], [NSString stringWithFormat:@"_%@",keyPath].UTF8String);
id value = object_getIvar(self, ivar);
// NSLog(@"old:%@",value);
((id (*) (id,SEL,id))objc_msgSend)(self,NSSelectorFromString(setMethod),arg);
id valueNew = arg;
// NSLog(@"new:%@",valueNew);
//isa指針指向子類
object_setClass(self, subClass);
void(^blkUse)(id old, id new) = objc_getAssociatedObject(self, "blk");
if (blkUse) {
blkUse(value,valueNew);
}
}
複製代碼
以上方法便可實現一個簡單的kvo了 3. 重寫class方法來隱藏內部子類
Class classUse(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
複製代碼
int _isKVOAUse(id self,SEL _cmd){
return YES;
}
複製代碼
上述方法便可實現一個kvo了,拿去用吧!
p1 = [[Person alloc] init];
__weak typeof(self) weakSelf = self;
[p1 addObserver:self forKeyPath:@"name" valueChangeBlk:^(id _Nonnull old, id _Nonnull new) {
typeof(weakSelf) self = weakSelf;
NSLog(@"self:%@,old:%@, new:%@",self,old,new);
}];
複製代碼
這裏我用__weak typeof(self) weakSelf = self;typeof(weakSelf) self = weakSelf;巧妙的解決了循環引用問題。這裏我是參考MJRefresh源碼,這樣在block內部就能夠繼續使用self關鍵字了。 4. 關於以上的疑惑但願有人能解答,thanks!thanks!thanks!最後附上github上源代碼給你們參考:github.com/FreeBaiShun…