2 RAC解析 自定義KVO

知識點概述

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動態添加對應的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);
}

六.代碼

代碼下載地址

相關文章
相關標籤/搜索