KVO的自定義

一.探索前需知

1.1 KVO的底層原理實現?

  • 一、當對對象A進行KVO觀察時候,會動態生成一個子類,而後將對象的isa指向新生成的子類
  • 二、KVO本質上是監聽屬性的setter方法,只要被觀察對象有成員變量和對應的set方法,就能對該對象經過KVO進行觀察
  • 三、子類會重寫父類的set、class、dealloc、_isKVOA方法
  • 四、當觀察對象移除全部的監聽後,會將觀察對象的isa指向原來的類
  • 五、當觀察對象的監聽所有移除後,動態生成的類不會註銷,而是留在下次觀察時候再使用,避免反覆建立中間子類
附上篇關於KVO原理分析的地址: juejin.im/post/5e4cf4…

 二. 自定義KVO的初探

2.1 KVO自定義準備

咱們先看下系統的KVO的實現:ios

@interface NSObject(NSKeyValueObserverRegistration)

/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong. */ - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; @end 複製代碼

原來KVO的實現是在NSObjectNSKeyValueObserverRegistration 分類裏.macos

因此咱們也實現個分類,在分類裏處理KVO的相關事宜:編程


接着咱們根據KVO的底層原理實現,開始一步步的自定義.api

2.2  動態生成一個子類,而後將對象的isa指向新生成的子類

從原理得知當對對象A進行KVO觀察時候,會動態生成一個子類,而後將對象的isa指向新生成的子類.因此應該在這方法裏:
安全

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: 驗證是否存在setter方法 : 不讓實例進來
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 動態生成子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    // 4: 保存觀察者
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
複製代碼

 Class newClass = [self createChildClassWithKeyPath:keyPath]; 動態生成子類時,咱們也要給新生成的子類重寫父類的set、class、dealloc、_isKVOA方法.bash

#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // 防止重複建立生成新類
    if (newClass) return newClass;
    /**
     * 若是內存不存在,建立生成
     * 參數一: 父類
     * 參數二: 新類的名字
     * 參數三: 新類的開闢的額外空間
     */
    // 2.1 : 申請類
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 註冊類
    objc_registerClassPair(newClass);
    // 2.3.1 : 添加class : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
    return newClass;
}
複製代碼

重寫class方法指向派生類父類async

Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
複製代碼

重寫setter方法:函數式編程

static void lg_setter(id self,SEL _cmd,id newValue){

}
複製代碼

2.3 重寫派生子類的set方法

在這裏你們和我一塊兒思考下,系統的KVO是怎麼觸發的?以前文章就已經說過要想了解KVO就必須先了解KVC,KVC的調用過程就會自動觸發鍵值觀察(KVO).好比:函數

[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.nickName = @"KC"; 
複製代碼

其本質是Person類底層調用了- (void)setNickName:(NSString *)nickName方法, post

這樣就會觸發KVO鍵值觀察.會進行KVO的回調

#pragma mark - KVO回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
複製代碼

因此在重寫Person類的派生子類setter方法時, 要讓Person也響應setter方法,再一個就是observer (觀察者)響應 自定義方法 - (void)lg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object。

因此在子類setter方法裏:轉發消息給父類,讓父類也實現setter方法,而後再給observer發送消息,響應

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"來了:%@",newValue);
    // 4: 消息轉發 : 轉發給父類
    // 改變父類的值 --- 能夠強制類型轉換
    
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    
    // 既然觀察到了,下一步不就是回調 -- 讓咱們的觀察者調用
    // - (void)lg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: 拿到觀察者
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    // 2: 消息發送給觀察者
    SEL observerSEL = @selector(lg_observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
    
}複製代碼

2.4 當觀察對象移除全部的監聽後,會將觀察對象的isa指向原來的類

- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    // 指回給父類
    Class superClass = [self class];
    object_setClass(self, superClass);
}複製代碼

到這裏爲止自定義KVO的基本功能就完成了.咱們來試驗下 :


看運行輸出:


是否是來了,說明咱們自定義的KVO已經成功了,但確定還有許多東西要優化,請接着往下面看.

三. 自定義KVO的進階(優化)

3.1 KVO裏的Observer用模型進行包裝,使代碼更具備面向對象的特色.

@interface LGInfo : NSObject
@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString    *keyPath;
@end
複製代碼

@implementation LGInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    if (self=[super init]) {
        _observer = observer;
        _keyPath  = keyPath;
    }
    return self;
}
@end
複製代碼

在添加觀察者時咱們就能夠:

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: 驗證是否存在setter方法 : 不讓實例進來
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 動態生成子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    // 4: 保存觀察者信息
    LGKVOInfo *info = [[LGKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    if (!observerArr) {
        observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}
複製代碼

 在子類setter方法裏:

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"來了:%@",newValue);
    // 4: 消息轉發 : 轉發給父類
    // 改變父類的值 --- 能夠強制類型轉換
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    // 1: 拿到觀察者
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 對新舊值進行處理
                if (info.options & LGKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & LGKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                // 2: 消息發送給觀察者
                SEL observerSEL = @selector(lg_observeValueForKeyPath:ofObject:change:context:);
                objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
            });
        }
    }
    
}
複製代碼

這樣代碼是否是更好一些,設計更合理.

3.2 KVO自定義中加入函數式編程設計思想

在剛剛的設計中咱們經過添加abserver以後,咱們還需從-(void)lg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object,獲得屬性的變化,這種設計方法是否是太麻煩了.咱們能夠加入函數式編程設計思想,用block直接回調屬性變化結果.這樣接口和API須要稍微修改下:

@interface LGInfo : NSObject
@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString    *keyPath;
@property (nonatomic, copy) LGKVOBlock  handleBlock;
@end

@implementation LGInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    if (self=[super init]) {
        _observer = observer;
        _keyPath  = keyPath;
        _handleBlock = block;
    }
    return self;
}
@end

複製代碼

lg_addObserver方法改爲: 

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block{

    // 1: 驗證是否存在setter方法 : 不讓實例進來

    [self judgeSetterMethodFromKeyPath:keyPath];

    // 2: 動態生成子類

    Class newClass = [self createChildClassWithKeyPath:keyPath];

    // 3: isa的指向 : LGKVONotifying_LGPerson

    object_setClass(self, newClass);

    // 4: 保存信息

    LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];

    

    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));

    if (!mArray) {

        mArray = [NSMutableArray arrayWithCapacity:1];

        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }

    [mArray addObject:info];
}
複製代碼

派生類中:lg_setter裏直接用block進行回調.

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"來了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息轉發 : 轉發給父類
    // 改變父類的值 --- 能夠強制類型轉換
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 5: 信息數據回調
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

複製代碼

補充下:這兩年蘋果底層不少api ,也加入了函數式編程思想.好比NSTimer

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0))複製代碼

3.3 KVO 自動銷燬機制

剛剛自定的時候,每次須要本身手動移除觀察者,有時候盆友可能忘了,會致使崩潰異常等一系列後果,在這裏咱們就想能不能自動銷燬.

在這裏咱們先思考下,該何時銷燬?

有的盆友確定會想到當觀察者(一般是VC)釋放時,就該銷燬對象的監聽屬性.不錯,其實能不能更靠前,當Person 的派生類對象都釋放的時候,咱們是否是就應該能夠銷燬了.

因此咱們能夠監聽Person 的派生類 dealloc 方法.可是這個dealloc 方法在NSOBject的分類裏,若是在這裏面之間操做的話、其它地方(VC)引用了這個頭文件沒有使用KVO的,當這個(VC)裏的NSObject對象釋放時也會來到這個方法. 

因此方法一:

還記得 createChildClassWithKeyPath ,這個動態實現子類的方法嗎?

在這裏咱們重寫了 set、class 方法 ,如今再重寫dealloc 方法,

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // 防止重複建立生成新類
    if (newClass) return newClass;
    /**
     * 若是內存不存在,建立生成
     * 參數一: 父類
     * 參數二: 新類的名字
     * 參數三: 新類的開闢的額外空間
     */
    // 2.1 : 申請類
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 註冊類
    objc_registerClassPair(newClass);
    // 2.3.1 : 添加class : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
    // 2.3.3 : 添加dealloc
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);

    return newClass;
}

static void lg_dealloc(id self,SEL _cmd){
    Class superClass = [self class];
    object_setClass(self, superClass);
}複製代碼

這樣就能夠自動釋放啦,因此你們知道爲何系統KVO在生成派生子類時,會重寫dealloc方法了吧.

第二種方法:

系統NSObject 原本就有dealloc方法,我爲啥要重寫?不重寫的話,若是在這裏面之間操做的話、其它地方(VC)引用了這個頭文件沒有使用KVO的,當這個(VC)裏的NSObject對象釋放時也會來到這個方法.因此咱們用到 MethodSwizzle.

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // 防止重複建立生成新類
    if (newClass) return newClass;
    /**
     * 若是內存不存在,建立生成
     * 參數一: 父類
     * 參數二: 新類的名字
     * 參數三: 新類的開闢的額外空間
     */
    // 2.1 : 申請類
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 註冊類
    objc_registerClassPair(newClass);
    // 2.3.1 : 添加class : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
//    // 2.3.3 : 添加dealloc
//    SEL deallocSEL = NSSelectorFromString(@"dealloc");
//    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
//    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
//    class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);
  
    // 方法交換
    [self kc_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
    
    return newClass;
}
複製代碼

- (BOOL)kc_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
 //   Class cls = self;
    Class cls = [self class];
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!swiMethod) {
        return NO;
    }
    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    return YES;
}
複製代碼

- (void)myDealloc{

    Class superClass = [self class];
    object_setClass(self, superClass);
    // 不會形成循環遞歸,下面調用myDealloc等於調用dealloc
    [self myDealloc];
}
複製代碼

四.總結

本文經過自定義實現一套簡化版的KVO進一步加深了對KVO實現原理的理解。固然一套完善的KVO並無這麼簡單,裏面還沒考慮到線程安全、鎖、觀察屬性以keypath等狀況。代碼中確定還有許多漏洞,在這裏給你們推薦比較成熟的自定義KVO.FaceBook開發設計好的(FBKVOController),你們有興趣能夠去看下.

相關文章
相關標籤/搜索