iOS底層-KVO底層原理

什么是KVO

KVO,全稱為Key-Value observing,中文名為鍵值觀察,KVO是一種機制,它允許將其他對象的指定屬性的更改通知給對象。
Key-Value Observing Programming Guide官方文檔中,又這么一句話:理解KVO之前,必須先理解KVC(即KVO是基于KVC基礎之上)

In order to understand key-value observing, you must first understand key-value coding.
KVC是鍵值編碼,在對象創建完成后,可以動態的給對象屬性賦值,而KVO是鍵值觀察,提供了一種監聽機制,當指定的對象的屬性被修改后,則對象會收到通知,所以可以看出KVO是基于KVC的基礎上對屬性動態變化的監聽

在iOS日常開發中,經常使用KVO來監聽對象屬性的變化,并及時做出響應,即當指定的被觀察的對象的屬性被修改后,KVO會自動通知相應的觀察者,那么KVO與NSNotificatioCenter有什么區別呢?

相同點
1、兩者的實現原理都是觀察者模式,都是用于監聽
2、都能實現一對多的操作

不同點
1、KVO只能用于監聽對象屬性的變化,并且屬性名都是通過NSString來查找,編譯器不會幫你檢測對錯和補全,純手敲會比較容易出錯
2、NSNotification的發送監聽(post)的操作我們可以控制,kvo由系統控制。
3、KVO可以記錄新舊值變化

KVO使用注意事項

1.基本使用
KVO的基本使用主要分為3步:
注冊觀察者addObserver:forKeyPath:options:context

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

實現KVO回調observeValueForKeyPath:ofObject:change:context

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}

移除觀察者removeObserver:forKeyPath:context

[self.person removeObserver:self forKeyPath:@"nick" context:NULL];

2、context使用

在官方文檔中,針對參數context有如下說明:


參數context官方說明.png

大致含義就是:addObserver:forKeyPath:options:context:方法中的上下文context指針包含任意數據,這些數據將在相應的更改通知中傳遞回觀察者。可以通過指定context為NULL,從而依靠keyPath即鍵路徑字符串傳來確定更改通知的來源,但是這種方法可能會導致對象的父類由于不同的原因也觀察到相同的鍵路徑而導致問題。所以可以為每個觀察到的keyPath創建一個不同的context,從而完全不需要進行字符串比較,從而可以更有效地進行通知解析

通俗的講,context上下文主要是用于區分不同對象的同名屬性,從而在KVO回調方法中可以直接使用context進行區分,可以大大提升性能,以及代碼的可讀性

context使用總結

不使用context,使用keyPath區分通知來源

//context的類型是 nullable void *,應該是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];

使用context區分通知來源

//定義context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

//注冊觀察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    
    
//KVO回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"%@",change);
    }else if (context == PersonNameContext){
        NSLog(@"%@",change);
    }
}

3.移除KVO通知的必要性
在官方文檔中,針對KVO的移除有以下幾點說明


KVO的移除.png

刪除觀察者時,請記住以下幾點:

要求被移除為觀察者(如果尚未注冊為觀察者)會導致NSRangeException。您可以對removeObserver:forKeyPath:context:進行一次調用,以對應對addObserver:forKeyPath:options:context:的調用,或者,如果在您的應用中不可行,則將removeObserver:forKeyPath:context:調用在try / catch塊內處理潛在的異常。

釋放后,觀察者不會自動將其自身移除。被觀察對象繼續發送通知,而忽略了觀察者的狀態。但是,與發送到已釋放對象的任何其他消息一樣,更改通知會觸發內存訪問異常。因此,您可以確保觀察者在從內存中消失之前將自己刪除。

該協議無法詢問對象是觀察者還是被觀察者。構造代碼以避免發布相關的錯誤。一種典型的模式是在觀察者初始化期間(例如,在init或viewDidLoad中)注冊為觀察者,并在釋放過程中(通常在dealloc中)注銷,以確保成對和有序地添加和刪除消息,并確保觀察者在注冊之前被取消注冊,從內存中釋放出來。

所以,總的來說,KVO注冊觀察者 和移除觀察者是需要成對出現的,如果只注冊,不移除,會出現類似野指針的崩潰,如下圖所示:


類似野指針的崩潰.png

崩潰的原因是,由于第一次注冊KVO觀察者后沒有移除,再次進入界面,會導致第二次注冊KVO觀察者,導致KVO觀察的重復注冊,而且第一次的通知對象還在內存中,沒有進行釋放,此時接收到屬性值變化的通知,會出現找不到原有的通知對象,只能找到現有的通知對象,即第二次KVO注冊的觀察者,所以導致了類似野指針的崩潰,即一直保持著一個野通知,且一直在監聽

4、KVO的自動觸發與手動觸發

KVO觀察的開啟和關閉有兩種方式,自動和手動
自動開關,返回NO,就監聽不到,返回YES,表示監聽

// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}

自動開關關閉的時候,可以通過手動開關監聽

- (void)setName:(NSString *)name{
    //手動開關
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

使用手動開關的好處就是你想監聽就監聽,不想監聽關閉即可,比自動觸發更方便靈活
5、KVO觀察:一對多
KVO觀察中的一對多,意思是通過注冊一個KVO觀察者,可以監聽多個屬性的變化

以下載進度為例,比如目前有一個需求,需要根據總的下載量totalData 和當前下載量currentData 來計算當前的下載進度currentProcess,實現有兩種方式

分別觀察 總的下載量totalData 和當前下載量currentData 兩個屬性,當其中一個發生變化計算 當前下載進度currentProcess

實現keyPathsForValuesAffectingValueForKey方法,將兩個觀察合為一個觀察,即觀察當前下載進度currentProcess

//1、合二為一的觀察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"currentProcess"]) {
        NSArray *affectingKeys = @[@"totalData", @"currentData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

//2、注冊KVO觀察
[self.person addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];

//3、觸發屬性值變化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.currentData += 10;
    self.person.totalData  += 1;
}

//4、移除觀察者
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"currentProcess"];
}

6、KVO觀察 可變數組
KVO是基于KVC基礎之上的,所以可變數組如果直接添加數據,是不會調用setter方法的,所有對可變數組的KVO觀察下面這種方式不生效的,即直接通過[self.person.dateArray addObject:@"1"];向數組添加元素,是不會觸發kvo通知回調的

//1、注冊可變數組KVO觀察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    
//2、KVO回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

//3、移除觀察者
- (void)dealloc{
 [self.person removeObserver:self forKeyPath:@"dateArray"];
}

//4、觸發數組添加數據
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person.dateArray addObject:@"1"];
}

在KVC官方文檔中,針對可變數組的集合類型,有如下說明,即訪問集合對象需要需要通過mutableArrayValueForKey方法,這樣才能將元素添加到可變數組中


kvc中關于集合的操作方法.png

修改

將4中的代碼修改如下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}

運行結果如下,可以看到,元素被添加到可變數組了


元素被添加到可變數組.png

其中的kind表示鍵值變化的類型,是一個枚舉,主要有以下4種

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//設值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替換
};

一般的屬性與集合的KVO觀察是有區別的,其kind不同,以屬性name 和 可變數組為例

屬性的kind一般是設值
可變數組的kind一般是插入


屬性與集合的kind區別.png

KVO 底層原理探索

官方文檔說明

在KVO的官方使用指南中,有如下說明


KVO原理-官方說明.png

KVO是使用isa-swizzling的技術實現的。

顧名思義,isa指針指向維護分配表的對象的類。該分派表實質上包含指向該類實現的方法的指針以及其他數據。

當為對象的屬性注冊觀察者時,將修改觀察對象的isa指針,指向中間類而不是真實類。結果,isa指針的值不一定反映實例的實際類。

您永遠不應依靠isa指針來確定類成員身份。相反,您應該使用class方法來確定對象實例的類。

代碼調試探索

1、KVO只對屬性觀察

在LGPerson中有一個成員變量name 和 屬性nickName,分別注冊KVO觀察,觸發屬性變化時,會有什么現象?

分別為成員變量name 和 屬性nickName注冊KVO觀察

self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
     [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

KVO通知觸發操作

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"KC";
     self.person->name    = @"Cooci";
}

運行結果如下:


KVO只對屬性觀察.png

結論:KVO對成員變量不觀察,只對屬性觀察,屬性和成員變量的區別在于屬性多一個 setter 方法,而KVO恰好觀察的是setter 方法

2、中間類

根據官方文檔所述,在注冊KVO觀察者后,觀察對象的isa指針指向會發生改變
注冊觀察者之前:實例對象person的isa指針指向LGPerson


注冊觀察者之前.png

注冊觀察者之后:實例對象person的isa指針指向NSKVONotifying_LGPerson

注冊觀察者之后.png

綜上所述,在注冊觀察者后,實例對象的isa指針指向由LGPerson類變為了NSKVONotifying_LGPerson中間類,即實例對象的isa指針指向發生了變化

2-1、判斷中間類是否是派生類 即子類?

那么這個動態生成的中間類NSKVONotifying_LGPerson和LGPerson類 有什么關系?下面通過代碼來驗證

可以通過下面封裝的方法,獲取LGPerson的相關類

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    
    // 注冊類的總數
    int count = objc_getClassList(NULL, 0);
    // 創建一個數組, 其中包含給定對象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 獲取所有已注冊的類
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[I]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

//********調用********
[self printClasses:[LGPerson class]];

打印結果如下所示:


中間類是子類.png

從結果中可以說明NSKVONotifying_LGPerson是LGPerson的子類

2-2、中間類中有什么?

可以通過下面的方法獲取NSKVONotifying_LGPerson類中的所有方法

#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[I];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

//********調用********
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];

輸出結果如下


中間類方法打印.png

從結果中可以看出有四個方法,分別是setNickName 、 class 、 dealloc 、 _isKVOA,這些方法是繼承還是重寫?

在LGStudent中重寫setNickName方法,獲取LGStudent類的所有方法


LGStudent方法打印.png

與中間類的方法進行的對比說明只有重寫的方法,才會在子類的方法列表中遍歷打印出來,而繼承的不會在子類遍歷出來

獲取LGPerson和NSKVONotifying_LGPerson的方法列表進行對比


對比.png

綜上所述,有如下結論:

NSKVONotifying_LGPerson中間類重寫了父類LGPerson的setNickName方法
NSKVONotifying_LGPerson中間類重寫了基類NSObject的class 、 dealloc 、 _isKVOA方法
其中dealloc是釋放方法
_isKVOA判斷當前是否是kvo類
2-3、dealloc中移除觀察者后,isa指向是誰,以及中間類是否會銷毀?

移除觀察者之前:實例對象的isa指向仍是NSKVONotifying_LGPerson中間類

移除觀察者之前.png

移除觀察者之后:實例對象的isa指向更改為LGPerson類


移除觀察者之后.png

所以,在移除kvo觀察者后,isa的指向由NSKVONotifying_LGPerson變成了LGPerson

那么中間類從創建后,到dealloc方法中移除觀察者之后,是否還存在?

在上一級界面打印LGPerson的子類情況,用于判斷中間類是否銷毀


中間類未銷毀.png

通過子類的打印結果可以看出,中間類一旦生成,沒有移除,沒有銷毀,還在內存中 -- 主要是考慮重用的想法,即中間類注冊到內存中,為了考慮后續的重用問題,所以中間類一直存在

總結

綜上所述,關于中間類,有如下說明:

實例對象isa的指向在注冊KVO觀察者之后,由原有類更改為指向中間類

中間類重寫了觀察屬性的setter方法、class、dealloc、_isKVOA方法

dealloc方法中,移除KVO觀察者之后,實例對象isa指向由中間類更改為原有類

中間類從創建后,就一直存在內存中,不會被銷毀

自定義KVO

自定KVO的流程,跟系統一致,只是在系統的基礎上針對其中的部分做了一些優化處理。

1、將注冊和響應通過函數式編程,即block的方法結合在一起
2、去掉系統繁瑣的三部曲,實現KVO自動銷毀機制
在系統中,注冊觀察者和KVO響應屬于響應式編程,是分開寫的,在自定義為了代碼更好的協調,使用block的形式,將注冊和回調的邏輯組合在一起,即采用函數式編程方式,還是分為三部分

注冊觀察者

//*********定義block*********
typedef void(^LGKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

//*********注冊觀察者*********
- (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block;

KVO響應
這部分主要是通過重寫setter方法,在中間類的setter方法中,通過block的方式傳遞給外部進行響應

移除觀察者

//*********移除觀察者*********
- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

準備條件:創建NSObject類的分類CJLJVO

注冊觀察者

在注冊觀察者方法中,主要有以下幾部分操作:

1、判斷當前觀察值keyPath的setter方法是否存在

#pragma mark - 驗證是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath
{
    Class superClass = object_getClass(self);
    SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"CJLKVO - 沒有當前%@的setter方法", keyPath] userInfo:nil];
    }
    
}

2、動態生成子類,將需要重寫的class方法添加到中間類中

#pragma mark - 動態生成子類
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    //獲取原本的類名
    NSString  *oldClassName = NSStringFromClass([self class]);
    //拼接新的類名
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kCJLKVOPrefix,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 添加方法
    
    SEL classSel = @selector(class);
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char *classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)cjl_class, classType);

    return newClass;
}

//*********class方法*********
#pragma mark - 重寫class方法,為了與系統類對外保持一致
Class cjl_class(id self, SEL _cmd){
    //在外界調用class返回CJLPerson類
    return class_getSuperclass(object_getClass(self));//通過[self class]獲取會造成死循環
}

3、isa指向由原有類,改為指向中間類

object_setClass(self, newClass);

4、保存信息:這里用的數組,也可以使用map,需要創建信息的model模型類

//*********KVO信息的模型類/*********
#pragma mark 信息model類
@interface CJLKVOInfo : NSObject

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

- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block;

@end
@implementation CJLKVOInfo

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

//*********保存信息*********
//- 保存多個信息
CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//使用數組存儲 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {//如果mArray不存在,則重新創建
    mArray = [NSMutableArray arrayWithCapacity:1];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];

完整的注冊觀察者代碼如下

#pragma mark - 注冊觀察者 - 函數式編程
- (void)cjl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    
    //1、驗證是否存在setter方法
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    //保存信息
    //- 保存多個信息
    CJLKVOInfo *info = [[CJLKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    //使用數組存儲 -- 也可以使用map
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if (!mArray) {//如果mArray不存在,則重新創建
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
    
    //判斷automaticallyNotifiesObserversForKey方法返回的布爾值
    BOOL isAutomatically = [self cjl_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
    if (!isAutomatically) return;
    
    //2、動態生成子類、
    /*
        2.1 申請類
        2.2 注冊
        2.3 添加方法
     */
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3、isa指向
    object_setClass(self, newClass);
    
    //獲取sel
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    //獲取setter實例方法
    Method method = class_getInstanceMethod([self class], setterSel);
    //方法簽名
    const char *type = method_getTypeEncoding(method);
    //添加一個setter方法
    class_addMethod(newClass, setterSel, (IMP)cjl_setter, type); 
}

注意點

關于objc_msgSend的檢查關閉:target -> Build Setting -> Enable Strict Checking of objc_msgSend Calls 設置為NO


設置.png

class方法必須重寫,其目的是為了與系統一樣,對外的類保持一致,如下所示
系統的KVO,在添加觀察者前后,實例對象person的類一直都是CJLPerson


系統KVO-注冊前后對比.png

如果沒有重寫class方法,自定的KVO在注冊前后的實例對象person的class就會看到是不一致的,返回的isa更改后的類,即中間類


自定義KVO-未重寫class方法的注冊前后對比.png

重寫后class方法后的自定義KVO,在注冊觀察者前后其實例對象類的顯示,與系統的顯示是一致的

KVO響應

主要是給子類動態添加setter方法,其目的是為了在setter方法中向父類發送消息,告知其屬性值的變化

5、將setter方法重寫添加到子類中(主要是在注冊觀察者方法中添加)

//獲取sel
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    //獲取setter實例方法
    Method method = class_getInstanceMethod([self class], setterSel);
    //方法簽名
    const char *type = method_getTypeEncoding(method);
    //添加一個setter方法
    class_addMethod(newClass, setterSel, (IMP)cjl_setter, type);

6、通過將系統的objc_msgSendSuper強制類型轉換自定義的消息發送cjl_msgSendSuper

//往父類LGPerson發消息 - 通過objc_msgSendSuper
//通過系統強制類型轉換自定義objc_msgSendSuper
void (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
//定義一個結構體
struct objc_super superStruct = {
    .receiver = self, //消息接收者 為 當前的self
    .super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的類 為 父類
};
//調用自定義的發送消息函數
cjl_msgSendSuper(&superStruct, _cmd, newValue);

7、告知vc去響應:獲取信息,通過block傳遞

/*---函數式編程*/
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    for (CJLKVOInfo *info in mArray) {
        NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            
           info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }

完整的setter方法代碼如下

static void cjl_setter(id self, SEL _cmd, id newValue){
    NSLog(@"來了:%@",newValue);
    
    //此時應該有willChange的代碼
    
    //往父類LGPerson發消息 - 通過objc_msgSendSuper
    //通過系統強制類型轉換自定義objc_msgSendSuper
    void (*cjl_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    //定義一個結構體
    struct objc_super superStruct = {
        .receiver = self, //消息接收者 為 當前的self
        .super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的類 為 父類
    };
    //調用自定義的發送消息函數
    cjl_msgSendSuper(&superStruct, _cmd, newValue);
    
    //此時應該有didChange的代碼
    
    //讓vc去響應
    /*---函數式編程*/
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    for (CJLKVOInfo *info in mArray) {
        NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            
           info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

移除觀察者

為了避免在外界不斷的調用removeObserver方法,在自定義KVO中實現自動移除觀察者

8、實現cjl_removeObserver:forKeyPath:方法,主要是清空數組,以及isa指向更改

- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    //清空數組
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if (mArray.count <= 0) {
        return;
    }
    
    for (CJLKVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [mArray removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }
    
    if (mArray.count <= 0) {
        //isa指回父類
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

9、在子類中重寫dealloc方法,當子類銷毀時,會自動調用dealloc方法(在動態生成子類的方法中添加)

#pragma mark - 動態生成子類
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    //...
    
    //添加dealloc 方法
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
    const char *deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)cjl_dealloc, deallocType);
    
    return newClass;
}

//************重寫dealloc方法*************
void cjl_dealloc(id self, SEL _cmd){
    NSLog(@"來了");
    Class superClass = [self class];
    object_setClass(self, superClass);
}

其原理主要是:CJLPerson發送消息釋放即dealloc了,就會自動走到重寫的cjl_dealloc方法中(原因是因為person對象的isa指向變了,指向中間類,但是實例對象的地址是不變的,所以子類的釋放,相當于釋放了外界的person,而重寫的cjl_dealloc相當于是重寫了CJLPerson的dealloc方法,所以會走到cjl_dealloc方法中),達到自動移除觀察者的目的

總結

綜上所述,自定義KVO大致分為以下幾步

注冊觀察者 & 響應
1、驗證是否存在setter方法

2、保存信息

3、動態生成子類,需要重寫class、setter方法

4、在子類的setter方法中向父類發消息,即自定義消息發送

5、讓觀察者響應

  • 移除觀察者
    • 1、更改isa指向為原有類

    • 2、重寫子類的dealloc方法

拓展

以上自定義的邏輯并不完善,只是闡述了KVO底層原來實現的大致邏輯,具體的可以參考facebook的KVO三方框架KVOController

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374