首先了解一下系統(tǒng)的KVO實(shí)現(xiàn)原理:其實(shí)就是動(dòng)態(tài)的創(chuàng)建了一個(gè)被觀察者的子類表牢,然后動(dòng)態(tài)修改它的isa指針指向它的子類盖喷,在子類里重寫屬性的set方法夯秃,最后在set方法里監(jiān)聽屬性變化愤惰,并發(fā)出通知包归。
1锨推、驗(yàn)證系統(tǒng)原理:
打個(gè)斷點(diǎn),發(fā)現(xiàn)實(shí)例s的isa是指向Student的公壤,然后單步執(zhí)行一下换可,
這時(shí)候發(fā)現(xiàn)實(shí)例s的isa指針變成了
NSKVONotifying_Student
,由此可見厦幅,當(dāng)我們添加觀察者的時(shí)候沾鳄,系統(tǒng)動(dòng)態(tài)的創(chuàng)建了一個(gè)子類NSKVONotifying_Student
,并把s的類型修改成了它确憨。
2译荞、接下來我們自己用RunTime來仿照系統(tǒng)KVO原理來自己寫一個(gè)KVO
首先創(chuàng)建一個(gè)NSObject的分類NSObject+KVO
,然后自己實(shí)現(xiàn)一個(gè)監(jiān)聽方法缚态。
#import "NSObject+KVO.h"
#import <objc/message.h>
static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";
@implementation NSObject (KVO)
-(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
Class newClass = [self createClass:keyPath];
object_setClass(self, newClass);
// 4.將觀察者與對象綁定
objc_setAssociatedObject(self, SJKVOAssiociateKey, observer, OBJC_ASSOCIATION_ASSIGN);
}
- (Class) createClass:(NSString*) keyPath {
// 1. 拼接子類名 / SJKVO_Student
NSString* oldName = NSStringFromClass([self class]);
NSString* newName = [NSString stringWithFormat:@"SJKVO_%@", oldName];
// 2. 創(chuàng)建并注冊類
Class newClass = NSClassFromString(newName);
if (!newClass) {
// 創(chuàng)建并注冊類
newClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
objc_registerClassPair(newClass);
// 動(dòng)態(tài)添加方法
// class
Method classMethod = class_getInstanceMethod([self class], @selector(class));
const char* classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, @selector(class), (IMP)SJ_class, classTypes);
}
// setter
NSString* setterMethodName = getSetter(keyPath);
SEL setterSEL = NSSelectorFromString(setterMethodName);
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char* setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)SJ_setKey, setterTypes);
return newClass;
}
Class SJ_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
void SJ_setKey(id self, SEL _cmd, id newValue) {
struct objc_super oldSuper = {self,class_getSuperclass([self class])};
// 修改屬性值
objc_msgSendSuper(&oldSuper, _cmd, newValue);
// 拿出觀察者
id observer = objc_getAssociatedObject(self, SJKVOAssiociateKey);
NSLog(@"---%@",newValue);
// 調(diào)用observer
NSString *methodName = NSStringFromSelector(_cmd);
NSString *key = getValueKey(methodName);
objc_msgSend(observer, sel_registerName("observeValueForKeyPath:ofObject:change:context:"),key,self,@{key:newValue},nil);
// [observer observeValueForKeyPath:key ofObject:self change:@{key:newValue} context:nil];
}
// key -> setter
static NSString * getSetter(NSString *keyPath){
if (keyPath.length <= 0) { return nil; }
NSString *firstString = [[keyPath substringToIndex:1] uppercaseString];
NSString *leaveString = [keyPath substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
// cmd -> key
NSString* getKey(NSString * cmd) {
if (cmd.length <= 0 || ![cmd hasPrefix:@"set"] || ![cmd hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, cmd.length-4);
NSString *getter = [cmd substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
return getter;
}
@end
然后在用我們自己的方法來添加一下觀察者磁椒,看是否能觀察到
- (void)viewDidLoad {
[super viewDidLoad];
Student *s = [[Student alloc] init];
// [s addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
[s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
self.s = s;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"+++%@",self.s.age);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
i++;
self.s.age = [NSString stringWithFormat:@"%ld",i];
}
通過打印可以看出跟系統(tǒng)的效果一樣。
基本都有注釋玫芦,我就不詳解了浆熔,里邊一些關(guān)于runtime的動(dòng)態(tài)函數(shù)和消息轉(zhuǎn)發(fā)函數(shù),在我之前關(guān)于runtime的文章里都有詳解,不了解可以參照前幾篇文章做基礎(chǔ)医增。
3慎皱、用block回調(diào)
用系統(tǒng)的回調(diào)會一個(gè)問題,就是如果觀察的屬性多了叶骨,在回調(diào)方法里需要先判斷是哪個(gè)對象的哪個(gè)屬性茫多,比較麻煩,但是用block回調(diào)的話忽刽,就省去了這些麻煩天揖,并且代碼邏輯更清晰,更緊湊跪帝。接下來代碼多了今膊,我們順便整理封裝一下。
首先定義一個(gè)block和一個(gè)可以有block的構(gòu)造方法:
typedef void(^ValueChangeBlock)(id observer, NSString* keyPath, id oldValue, id newValue);
@interface NSObject (KVO)
// 系統(tǒng)回調(diào)
- (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// block回調(diào)
- (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock;
@end
然后在.m文件內(nèi)部創(chuàng)建一個(gè)類伞剑,來存儲要監(jiān)聽的信息:
static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";
@interface SJInfo : NSObject
@property (nonatomic, weak) NSObject* observer;
@property (nonatomic, strong) NSString* keyPath;
@property (nonatomic, copy) ValueChangeBlock valueChangeBlock;
@end
@implementation SJInfo
- (instancetype) initWithObserver:(NSObject*)observer forKeyPath:(NSString*) keyPath valueChangeBlock:(ValueChangeBlock) block {
if (self == [super init]) {
_observer = observer;
_keyPath = keyPath;
_valueChangeBlock = block;
}
return self;
}
@end
然后在創(chuàng)建是就把參數(shù)和block保存到數(shù)組里斑唬,以便下邊拿出來調(diào)用:
-(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock {
Class newClass = [self createClass:keyPath];
object_setClass(self, newClass);
// 信息保存
SJInfo* info = [[SJInfo alloc] initWithObserver:observer forKeyPath:keyPath valueChangeBlock:valueChangeBlock];
NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
if (!array) {
array = [NSMutableArray array];
objc_setAssociatedObject(self, SJKVOAssiociateKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[array addObject:info];
}
void SJ_setKey(id self, SEL _cmd, id newValue) {
struct objc_super oldSuper = {self,class_getSuperclass(object_getClass(self))};
// 獲取key
NSString *key = getKey(NSStringFromSelector(_cmd));
// 獲取舊值
id oldValue = objc_msgSendSuper(&oldSuper, NSSelectorFromString(key));
// 修改屬性值
objc_msgSendSuper(&oldSuper, _cmd, newValue);
NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
if (array) {
for (SJInfo* info in array) {
if ([info.keyPath isEqualToString:key]) {
info.valueChangeBlock(info.observer, key, oldValue, newValue);
return;
}
}
}
}
在setKey方法里拿出數(shù)組中的信息,找到對應(yīng)的key黎泣,然后block回調(diào)就可以了恕刘,這樣就可以同時(shí)監(jiān)聽多個(gè)屬性了。
銷毀觀察者
其實(shí)銷毀觀察者就是把isa指針指從子類回到原來就可以了抒倚,我們把他放在dealloc方法里來做比較合適褐着,有兩種方法:1.利用hook在創(chuàng)建子類的方法里做方法交換,把dealloc的方法實(shí)現(xiàn)指向自己的方法里衡便,然后做isa指回献起;2.也是在創(chuàng)建子類的時(shí)候同時(shí)動(dòng)態(tài)添加自己的dealloc方法來做,我們這里就用第二種實(shí)現(xiàn)一下:
放在子類的創(chuàng)建方法里镣陕,保證只創(chuàng)建一次
// 添加SJ_Dealloc谴餐,銷毀觀察者
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char* deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)SJ_Dealloc, deallocTypes);
void SJ_Dealloc(id self, SEL _cmd) {
// 父類
Class superClass = [self class];//class_getSuperclass(object_getClass(self));
object_setClass(self, superClass);
}
最后在外邊調(diào)用一下,非常方便
[s SJ_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
NSLog(@"oldValue ---- %@, newValue ---- %@", oldValue, newValue);
}];
[s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
NSLog(@"oldValue ++++ %@, newValue ++++ %@", oldValue, newValue);
}];