1.KVO实现原理
2.runtime使用html
给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。git
添加观察者的方法中github
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
会用runtime的方式手动建立一个其子类,而且将该对象变为该子类。该子类会复写观察方法中keyPath的setter方法,使这个setter被调用时利用runtime去调用observer的回调方法api
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
这里只作KVO的基本功能,当被观察者改变属性的时候通知观察者,因此定义以下方法缓存
NSObject+SQKVO.happ
/** 添加观察者 @param observer 观察者 @param keyPath 被观察的属性名 */ - (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; /** 当被观察的观察属性改变的时候的回调函数 @param keyPath 所观察被观察者的属性名 @param object 被观察者 @param value 被观察的属性的新值 */ - (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value; @end
由于这里要用到runtime因此须要添加runtime的头文件ide
#import <objc/message.h>
并且由于用到objc_msgSend因此要改变一下工程的环境变量函数
在被观察者调用- SQ_addObserver:forKeyPath:时首先动态生成一个其子类。ui
// 1.生成子类 // 1.1获取名称 Class selfClass = [self class]; NSString *className = NSStringFromClass(selfClass); NSString *KVOClassName = [className stringByAppendingString:@"_SQKVO"]; const char *KVOClassNameChar = [KVOClassName UTF8String]; // 1.2建立子类 Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0); // 1.3注册 objc_registerClassPair(KVOClass);
这里能够看到,咱们将子类的类名命名为“类名”+“_SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。
这里有个注意点,通常为动态建立的类名应尽可能复杂一些避免重复。最好加上“_”。编码
举个例子,若是用户给的keyPath是name,应该动态添加一个-setName:的方法。而这个setter的名字是 "set" + "把keyPath变为首字母大写" + ":"
因此能够得出
NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]]; SEL setter = NSSelectorFromString(setterString);
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
void setValue(id self, SEL _cmd, id newVale) { }
所添加方法的编码类型。setter的返回值是void,参数是一个对象(id)。void用"v"表示,返回值和参数之间用“@:”隔开,对象用"@"表示。最后咱们能够得出结果"v@:@"。
具体其余的编码类型能够参考苹果文档。
ps: 这里说下为何返回值和参数之间用“@:”隔开。“:”表明字符串,全部的OC方法都有两个隐藏参数在参数列表的最前面,“发起者”和 “方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个types实际上是imp返回值和参数的编码。由于OC方法中返回值和参数之间必然有“发起者”和“SEL”隔着,因此“@:”天然而然就成了返回值和参数之间的分隔符。
固然咱们还能够用@encode来获得咱们想要的编码类型
NSString *encodeString = [NSString stringWithFormat:@"%s%s%s%s", @encode(void), @encode(id), @encode(SEL), @encode(id)];
object_setClass(self, KVOClass);
用下面这个函数能够很方便的将一个对象用键值对的方式绑定到一个目标对象上。
*若是想了解跟多能够查找《Effective Objective-C》的第10条
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
目标对象
绑定对象的键,至关于NSDictionary的key
这里的key通常采用下面的方式声明:
static const void *SQKVOObserverKey = &SQKVOObserverKey; static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;
这样作是由于若想令两个键匹配到同一个值,则二者必须是彻底相同的指针才行。
绑定对象,至关于NSDictionary的value
绑定对象的缓存策略
@property (nonatomic, weak) :OBJC_ASSOCIATION_ASSIGN
@property (nonatomic, strong) :OBJC_ASSOCIATION_RETAIN_NONATOMIC
@property (nonatomic, copy) :OBJC_ASSOCIATION_COPY_NONATOMIC
@property (atomic, strong) :OBJC_ASSOCIATION_RETAIN
@property (atomic, weak) :OBJC_ASSOCIATION_COPY
最后关联的代码:
objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
这个函数的目的主要是:
1.利用objc_msgSend触发原先类的setter
2.利用objc_msgSend触发观察者的回调方法
// 保存子类 Class KVOClass = [self class]; // 变回原先的类型,去触发setter object_setClass(self, class_getSuperclass(KVOClass)); NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey); NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]]; SEL setter = NSSelectorFromString(setterString); objc_msgSend(self, setter, newVale);
id observer = objc_getAssociatedObject(self, SQKVOObserverKey); objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);
object_setClass(self, KVOClass);
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value { }
恭喜你看到这里,而且恭喜你已经成功了!
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.name = @"A"; [self SQ_addObserver:self forKeyPath:@"name"]; self.name = @"B"; } - (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value { NSLog(@"%@.%@=%@", object, keyPath, value); }