2.RAC解析 - 自定義KVO
知識點概述
1.KVO實現原理
2.runtime使用
目的
給NSObject新增一個Category,用於給例項物件新增觀察者,當該例項物件的某個屬性發生變化的時候通知觀察者。
大體思路
新增觀察者的方法中
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
會用runtime的方式手動建立一個其子類,並且將該物件變為該子類。該子類會複寫觀察方法中keyPath的setter方法,使這個setter被呼叫時利用runtime去呼叫observer的回撥方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
實現
這裡只做KVO的基本功能,當被觀察者改變屬性的時候通知觀察者,所以定義如下方法
NSObject+SQKVO.h
/** 新增觀察者 @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的標頭檔案
#import <objc/message.h>
而且因為用到objc_msgSend所以要改變一下工程的環境變數

##
一.動態生成子類
在被觀察者呼叫- SQ_addObserver:forKeyPath:時首先動態生成一個其子類。
// 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動態新增對應的setter
1 確定setter的名字
舉個例子,如果使用者給的keyPath是name,應該動態新增一個-setName:的方法。而這個setter的名字是 "set" + "把keyPath變為首字母大寫" + ":"
所以可以得出
NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]]; SEL setter =NSSelectorFromString(setterString);
2 利用class_addMethod()給子類動態新增方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
- cls:
給哪個類新增方法。即新生成的子類,上面生成的 KVOClass。 - name:
所新增方法的名稱。即上一步生成的字串 setterString。 - imp:
所新增方法的實現。即這個方法的C語言實現,首先在下面先寫一個C語言的方法。稍後會講具體實現。
void setValue(id self, SEL _cmd, id newVale) { }
-
types:
所新增方法的編碼型別。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)];
3 將當前物件的類變為我們所建立的子類的型別,即更改isa指標
object_setClass(self, KVOClass);
4 將keyPath和觀察者關聯(associate)到我們的物件上
用下面這個函式可以很方便的將一個物件用鍵值對的方式繫結到一個目標物件上。
*如果想了解跟多可以查詢《Effective Objective-C》的第10條
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
-
object
目標物件
-
key
繫結物件的鍵,相當於NSDictionary的key
這裡的key一般採用下面的方式宣告:
static const void *SQKVOObserverKey = &SQKVOObserverKey; static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;
這樣做是因為若想令兩個鍵匹配到同一個值,則兩者必須是完全相同的指標才行。
-
value
繫結物件,相當於NSDictionary的value
-
policy
繫結物件的快取策略
@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);
三.setValue()的實現
這個函式的目的主要是:
1.利用objc_msgSend觸發原先類的setter
2.利用objc_msgSend觸發觀察者的回撥方法
1. 觸發原先的setter方法
// 儲存子類 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);
2. 呼叫觀察者的回撥方法
id observer = objc_getAssociatedObject(self, SQKVOObserverKey); objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);
3.改回KVO類
object_setClass(self, KVOClass);
四.實現空的回撥方法
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value { }
五.呼叫自定義的KVO
恭喜你看到這裡,並且恭喜你已經成功了!
- (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); }