说好的风雨无阻,又拖延了3天,代码早都整理好了,但是没有整理成文章。这是上周日的任务~
说起来KVO(Key-Value Observing)
,肯定都用过,经常用来监听某一个值的变化。
照常,我们有一个Book
类。
//Book.h
@interface Book : NSObject
@property (nonatomic,strong) NSString *name;
@end
//Book.m
@implementation Book
@end
现在需要在ViewController
中监听Book
属性name
的变化~
//ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
//初始化
_book1 = [[Book alloc] init];
[_book1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
_book1.name = @"Tom";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSString *name = change[NSKeyValueChangeNewKey];
NSLog(@"%@",name);
}
这是我们通常监听一个类的一个属性,但是呢,偶然发现了一个好玩的事情,激发了对KVO
原理的研究。
在observeValueForKeyPath:ofObject:change:context
的方法中,打印一下Book
类的实例对象_book1
的类名。
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
//打印类名称
NSLog(@"KVO类名称:%s %@ ",object_getClassName(_book1) ,[_book1 class]);
}
2019-08-14 16:28:36.745982+0800 自定义KVO[13369:6261090]
_book1 getClassName = :NSKVONotifying_Book
_book1 class = Book
emmmm?这到底发生了什么?苹果又干了什么见不得光的事情?然后看一下是不是addObserver:forKeyPath:options:context:
发生了点什么事情?
输出(断点)一下addObserver
前后。
//方法编号
SEL sel = NSSelectorFromString(@"setName:");
NSLog(@"监听之前-----setName:%p 类名称:%s",[_book1 methodForSelector:sel],object_getClassName(_book1));
[_book1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"监听之后-----setName:%p 类名称:%s",[_book1 methodForSelector:sel],object_getClassName(_book1));
_book1.name = @"Tom";
2019-08-14 16:28:36.744947+0800 自定义KVO[13369:6261090] 监听之前-----setName:0x1079c5ef0 类名称:Book
2019-08-14 16:28:36.745478+0800 自定义KVO[13369:6261090] 监听之后-----setName:0x107d20b5e 类名称:NSKVONotifying_Book
打印了一下方法地址和_book1
的类名称,发现都不一样了,出现了我们刚才打印的类名称NSKVONotifying_Book
,接着看看NSKVONotifying_Book
这个类。
发现了NSKVONotifying_Book
的父类是Book
,呵!苹果。
也就是说当我们监听了
Book
类的name
之后,Runtime
动态生成了一个Book
类的子类NSKVONotifying_Book
并且重写了setName:
方法。
明白了原理,我们自己动手玩玩~。
自定义custom_addObserver:forKeyPath:options:context:
方法,之后调用我们自己的方法。
//Book.m
NSString *getVauleKeyPath(NSString *methodName);
void customSetName(id self, SEL _cmd , NSString *name){
/*我们肯定要发消息给它
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
*/
//既然我们重写了setName:方法,当然self.name 就不会赋值了,我们需要自己发消息了
//id self : CustomKVONotifying_Book 父类是 Book 我们需要向父类发送setName:的消息
struct objc_super superClass = {
self,
class_getSuperclass([self class])
};
((void (*)(id,SEL,NSString *))objc_msgSendSuper)((__bridge id)(&superClass), _cmd, name);
///给我们的观察者发消息
//发消息的对象
id observer = objc_getAssociatedObject(self, "observer");
//我们的Key
NSString *methodName = NSStringFromSelector(_cmd);
NSString *keyPath = getVauleKeyPath(methodName);
//变更的值
NSDictionary <NSKeyValueChangeKey,id> * change = @{NSKeyValueChangeNewKey:name};
((void (*)(id,SEL, NSString * , id , id , void *))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:) ,keyPath, self, change, nil);
}
NSString *getVauleKeyPath(NSString *methodName){
//重写的是setName:方法 我们的keyPath 当然是 ”name”了
//'set' 和 ':' 不要
NSRange range = NSMakeRange(3, methodName.length - 4);
//keyPath 当前是 'Name';需要首字母小写
NSString *keyPath = [methodName substringWithRange:range];
NSString *first = [[keyPath substringToIndex:1] lowercaseString];
//替换
keyPath = [keyPath stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:first];
return keyPath;
}
@implementation Book
- (void)custom_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
///先创建类
//当前类名
NSString *currentClass = [NSString stringWithUTF8String:object_getClassName(self)];
//根据KVO命名法,NSKVONotifying_Book 我们改一下
NSString *custom_Class = [NSString stringWithFormat:@"CustomKVONotifying_%@",currentClass];
//创建类
Class cutsom_Class = objc_allocateClassPair([self class], custom_Class.UTF8String, 0);
//向系统注册类,不然怎么用??
objc_registerClassPair(cutsom_Class);
//修改isa指针 指向我们的子类,不然无法处理我们发送的setName:消息
object_setClass(self, cutsom_Class);
//重写SetName方法,我们来处理。
//1、首先获取key keyPath,要监听的对象
//keyPath = name, capitalizedString 首字母大写 setName:
NSString *methodName = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
//方法编号
SEL sel = NSSelectorFromString(methodName);
//add_Method,照旧,之前文章有详细的描述。(参考消息转发机制)
class_addMethod(cutsom_Class, sel, (IMP)customSetName, "v@:@");
//将监听者绑定到当前类,以便于之后获取监听者,给监听者发消息。
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
@end
代码和注释都很清晰了,接下来我们整理一下:
1、首先我们需要获取当前类名称object_getClassName
;
2、我们子类的类名CustomKVONotifying_Book
;
3、创建我们的子类objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes)
;
4、向系统注册我们的子类objc_registerClassPair(Class _Nonnull cls)
;
5、修改我们的isa
指针,因为我们需要子类实现setName:
方法。
6、动态添加方法class_addMethod
,setName:
;
7、将我们的观察者绑定到当前类,以便于后续获取观察者,像观察者发消息;
8、自己实现setName:
;
9、自己实现了setName:
我们需要修改Book
类name
的值,通过调用向父类发消息的方法objc_msgSendSuper
;
10、向我们绑定的观察者发消息objc_msgSend
;
这样我们基本复刻了一下KVO苹果见不得光的事情。
网友评论