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); }