KVO探索

KVO歸納

你們都知道kvo是一種設計模式,是一種鍵值觀察,當屬性的值改變時候會觸發回調,獲取該屬性的舊值和新值。可是可能有些朋友不清楚何時用它,使用場景是什麼。當須要監聽一個屬性的值改變時候咱們能夠用到它。好比:git

  1. 當圖片的url改變時候自動加載新的圖片。
  2. 當scrollView的offset改變時獲得回調獲取offset的值,這時候不用delegate較好,特別是封裝一個框架時候,若是用delegate那麼框架的使用者也可能用它的delegate致使框架的delegate不會執行,可是kvo不會出現這種問題。
  3. 好比監聽一段mp3聲音進度的屬性,根據改變的值來進行UI繪製。
  4. 好比監聽一些開關量進行繪製UI等等。可見KVO仍是很經常使用的,也很實用。

KVO的簡單使用

  1. 監聽Person的對象p1的name屬性值的變化
//註冊kvo
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];

//屬性賦值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    count++;
    p1.name = [NSString stringWithFormat:@"%d",count];
}

//釋放
- (void)dealloc{
    [p1 removeObserver:self forKeyPath:@"name"];
}
複製代碼
  1. 手動kvo,能夠手動控制是否觸發kvo的回調
//重寫Person類的automaticallyNotifiesObserversForKey返回NO即關閉了自動kvo
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
@end

//註冊kvo
[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];

//屬性值變化(其實只要該對象的成員變量的值改變便可)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    count++;
    [p1 willChangeValueForKey:@"name"];
    p1.name = [NSString stringWithFormat:@"%d",count];
    [p1 didChangeValueForKey:@"name"];
}

//kvo回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
}

//釋放
- (void)dealloc{
    [p1 removeObserver:self forKeyPath:@"name"];
}
複製代碼

KVO的底層實現探索

系統的kvo是怎麼實現的呢?爲何只要對象的屬性變化了就會觸發回調呢?我也很好奇,在查看資料以前,本身也考慮了一下怎麼實現。
首先系統的kvo任意一個對象均可以調用addObserver方法,能夠肯定應該是NSObject的分類,並新增了這個addObserver的方法。接下來想到的是hook,好比hook Person類對應的setter方法。利用runTime方法交換實如今調用這個setter方法以前獲取該屬性對應的成員變量的值,獲得舊值。以後再調用該setter方法以後再次獲取該屬性對應的成員變量的值,獲得新值。最後再調用observeValueForKeyPath方法把新舊值傳遞給observe對應的類。github

接下來查看資料,Oh,My God 並非本身想的那樣,系統在調用addObserver方法時候動態的建立了一個新的子類繼承該被監聽的對象所對應的類。並重寫了父類的setter方法。並把對象的isa指針從父類指向了該子類。這樣當父類的對象調用setter方法時候就會調用子類的setter方法,在該setter方法內部調用了willChangeValueForKey,didChangeValueForKey方法。以後系統會調用observeValueForKeyPath方法,把舊的和新的值傳遞給oberver所對應的類。在新的子類裏除了重寫了父類的setter方法之外還重寫了class方法,該方法是爲了外界調用class時候隱藏新建立的子類。有一點很奇怪當咱們在addObserver方法後打一個斷點時並把鼠標光標移動到改對象上會發現它竟然顯示的是父類而不是新生成的子類。按理來講咱們把isa指針指向新的子類後該對象應該就屬於子類的實例纔對。設計模式

再以後我來驗證了一下,打印一下isa指針指向的類bash

NSLog(@"p1:%@",object_getClass(p1));
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    NSLog(@"p1:%@",object_getClass(p1));
複製代碼

打印結果:app

2019-01-29 14:28:40.983065+0800 KVOCustom[20946:172543] p1:Person
2019-01-29 14:28:40.983401+0800 KVOCustom[20946:172543] p1:NSKVONotifying_Person
複製代碼

發現確實在addOberver方法調用前是指向了Person類,在調用後指向了新類NSKVONotifying_Person,從而證實了addOberver方法內部確實是建立了新類。
以後固然是想了解下這個系統建立的新的類內部實現了哪些方法啦。本身寫了一個打印類內部方法以下:框架

- (void)printMethods:(Class)cls{
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *strM = [NSMutableString string];
    [strM appendString:[NSString stringWithFormat:@"%@: ",cls]];
    
    for (int i  = 0; i < count; i++) {
        Method method = methods[i];
        NSString *strMethodName = NSStringFromSelector(method_getName(method));
        [strM appendString:strMethodName];
        [strM appendString:@", "];
    }
    NSLog(@"%@",strM);
}
複製代碼

以後咱們調用方法打印函數

[p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    [p1 printMethods:object_getClass(p1)];
複製代碼

打印結果:ui

2019-01-29 14:35:32.435852+0800 KVOCustom[21016:175547] NSKVONotifying_Person: setName:, class, dealloc, _isKVOA,
複製代碼

說明系統新建立的類內部實現了setName方法(用來重寫父類的setter方法),class方法(爲了隱藏子類),dealloc方法(釋放內存),_isKVOA方法(是系統的kvo方法)url

當咱們探索到這裏基本上也就請楚了系統的kvo的實現。spa

結論

  1. 系統調用addObserver方法時候,內部新建立了一個類NSKVONotifying_Person繼承於Person類。而且改變了isa的指向,指向了這個新類。
  2. 新類裏邊重寫了父類的setter方法,這樣當person對象的name屬性賦值時候,也就是調用了setter方法,這時候會調用子類的setter方法,該方法猜想內部調用了willChangeValueForKey,didChangeValueForKey方法,這樣會調用oberver對應類的observeValueForKeyPath方法。將新舊的值都傳遞過來。
  3. 新類重寫了class方法,猜想徹底是爲了隱藏子類的實現。當person對象調用class方法時候將返回Person類而不是新的類猜想內部實現應該是這樣的:
Class classUse(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
複製代碼
  1. 新類調用了內部方法_isKVOA,猜想是爲了告知系統使用了kvo,內部方法實現猜想是這樣的:
int _isKVOAUse(id self,SEL _cmd){
    return YES;
}
複製代碼

本身手寫一個KVO

如今是否是手癢癢想本身手寫個KVO啦,如今咱們清楚了系統KVO的實現,模仿它咱們本身實現一個KVO吧。

  1. 首先肯定這個KVO類確定是NSObject類的分類(由於全部對象均可以調用addObserver方法)在其內部模仿系統的addObserver方法本身寫一個相似的
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath valueChangeBlk:(void(^)(id old, id new))valueChangeBlk{
    //建立子類
    NSString *oldClass = NSStringFromClass(self.class);
    NSString *newClass = [NSString stringWithFormat:@"BSKVONotify_%@",oldClass];
    Class classNew = objc_allocateClassPair(self.class, newClass.UTF8String, 16);
    objc_registerClassPair(classNew);
    object_setClass(self, NSClassFromString(newClass));
    
    //新增set方法
    NSMutableString *strM = [NSMutableString string];
    [strM appendString:[[keyPath substringToIndex:1] uppercaseString]];
    [strM appendString:[keyPath substringFromIndex:1]];
    NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM];
    class_addMethod(NSClassFromString(newClass), NSSelectorFromString(setMethod), (IMP)keyPathMethod,"v@:@");
    
    //新增class方法
    class_addMethod(classNew, NSSelectorFromString(@"class"), (IMP)classUse,"#@:");
    
    //新增_isKVOA方法
    class_addMethod(classNew, NSSelectorFromString(@"_isKVOA"),(IMP)_isKVOAUse, "i@:");
    
    //設置關聯對象
    objc_setAssociatedObject(self, "keyPath", keyPath, OBJC_ASSOCIATION_COPY);
    objc_setAssociatedObject(self, "blk", valueChangeBlk, OBJC_ASSOCIATION_COPY);
    objc_setAssociatedObject(self, "classNew", classNew, OBJC_ASSOCIATION_RETAIN);
    objc_setAssociatedObject(self, "classOld", self.class, OBJC_ASSOCIATION_RETAIN);
}
複製代碼
  1. 在寫一個對應的setter方法的函數做爲屬性值變化的回調
void keyPathMethod(id self,IMP _cmd, id arg){
    //set方法名,原始類和子類
    NSString *keyPath = objc_getAssociatedObject(self, "keyPath");
    NSMutableString *strM = [NSMutableString string];
    [strM appendString:[[keyPath substringToIndex:1] uppercaseString]];
    [strM appendString:[keyPath substringFromIndex:1]];
    NSString *setMethod = [NSString stringWithFormat:@"set%@:",strM];
    Class subClass = objc_getAssociatedObject(self, "classNew");
    Class oldClass = objc_getAssociatedObject(self,"classOld");
    
    //isa指針指向父類,執行set方法
    object_setClass(self, oldClass);
    //獲取成員變量的值
    Ivar ivar = class_getInstanceVariable([self class], [NSString stringWithFormat:@"_%@",keyPath].UTF8String);
    id value = object_getIvar(self, ivar);
   // NSLog(@"old:%@",value);
    ((id (*) (id,SEL,id))objc_msgSend)(self,NSSelectorFromString(setMethod),arg);
    id valueNew = arg;
   // NSLog(@"new:%@",valueNew);
    
    //isa指針指向子類
    object_setClass(self, subClass);
    
    void(^blkUse)(id old, id new) = objc_getAssociatedObject(self, "blk");
    if (blkUse) {
        blkUse(value,valueNew);
    }
}
複製代碼

以上方法便可實現一個簡單的kvo了 3. 重寫class方法來隱藏內部子類

Class classUse(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

複製代碼
  1. 新增_isKVO方法
int _isKVOAUse(id self,SEL _cmd){
    return YES;
}
複製代碼

上述方法便可實現一個kvo了,拿去用吧!

關於系統KVO和本身實現的KVO對比的疑惑

  1. 最開始有提到,系統的kvo當咱們打斷點時候發現系統的kvo對象竟然是父類就是原始類而不是新的子類。而咱們的kvo對象打斷點發現是新的子類而不是原始類。也就是說系統很好的隱藏了子類,而咱們寫的kvo卻作不到,目前還不清楚這個原理。
  2. void keyPathMethod(id self,IMP _cmd, id arg)你們有沒有注意到這裏參數寫的是id類型,這裏的參數對應接收setter方法傳遞進來的參數。這個參數目前接收的是id類型,這樣就有侷限了,這樣監聽的對象的屬性類型就必定是對象類型了,若是是基本類型就會崩潰。這裏使用時候也要注意了,目前我不清楚該怎麼改,怎麼能同時接收兩種類型。
  3. 注意循環引用的問題
p1 = [[Person alloc] init];
    __weak typeof(self) weakSelf = self;
    [p1 addObserver:self forKeyPath:@"name" valueChangeBlk:^(id  _Nonnull old, id  _Nonnull new) {
        typeof(weakSelf) self = weakSelf;
        NSLog(@"self:%@,old:%@, new:%@",self,old,new);
    }];
複製代碼

這裏我用__weak typeof(self) weakSelf = self;typeof(weakSelf) self = weakSelf;巧妙的解決了循環引用問題。這裏我是參考MJRefresh源碼,這樣在block內部就能夠繼續使用self關鍵字了。 4. 關於以上的疑惑但願有人能解答,thanks!thanks!thanks!最後附上github上源代碼給你們參考:github.com/FreeBaiShun…

相關文章
相關標籤/搜索