iOS-底層原理 23:KVO 底層原理

iOS 底層原理 文章匯總

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是鍵值編碼沃但,在對象創(chuàng)建完成后,可以動態(tài)的給對象屬性賦值淤刃,而KVO是鍵值觀察,提供了一種監(jiān)聽機制铝侵,當指定的對象的屬性被修改后,則對象會收到通知疟丙,所以可以看出KVO是基于KVC的基礎上對屬性動態(tài)變化的監(jiān)聽

在iOS日常開發(fā)中览祖,經(jīng)常使用KVO來監(jiān)聽對象屬性的變化,并及時做出響應玄货,即當指定的被觀察的對象的屬性被修改后夹界,KVO會自動通知相應的觀察者鸠踪,那么KVONSNotificatioCenter有什么區(qū)別呢?

  • 相同點
    • 1评汰、兩者的實現(xiàn)原理都是觀察者模式,都是用于監(jiān)聽

    • 2、都能實現(xiàn)一對多的操作

  • 不同點
    • 1坯墨、KVO只能用于監(jiān)聽對象屬性的變化,并且屬性名都是通過NSString來查找液斜,編譯器不會幫你檢測對錯和補全,純手敲會比較容易出錯

    • 2臼膏、NSNotification發(fā)送監(jiān)聽(post)的操作我們可以控制,kvo系統(tǒng)控制始鱼。

    • 3、KVO可以記錄新舊值變化

KVO 使用注意事項

1会烙、基本使用

KVO的基本使用主要分為3步:

  • 注冊觀察者addObserver:forKeyPath:options:context
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • 實現(xiàn)KVO回調(diào)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使用

在官方文檔中沃缘,針對參數(shù)context有如下說明:

參數(shù)context官方說明

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

通俗的講,context上下文主要是用于區(qū)分不同對象的同名屬性牛柒,從而在KVO回調(diào)方法中可以直接使用context進行區(qū)分椭更,可以大大提升性能湿滓,以及代碼的可讀性

context使用總結

  • 不使用context,使用keyPath區(qū)分通知來源
//context的類型是 nullable void *朝氓,應該是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
  • 使用context區(qū)分通知來源
//定義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回調(diào)
- (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的移除

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

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

  • 釋放后跨释,觀察者不會自動將其自身移除。被觀察對象繼續(xù)發(fā)送通知,而忽略了觀察者的狀態(tài)龄恋。但是,與發(fā)送到已釋放對象的任何其他消息一樣显押,更改通知會觸發(fā)內(nèi)存訪問異常挖息。因此,您可以確保觀察者在從內(nèi)存中消失之前將自己刪除电禀。

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

所以螺句,總的來說,KVO注冊觀察者 和移除觀察者是需要成對出現(xiàn)的取劫,如果只注冊勇凭,不移除,會出現(xiàn)類似野指針的崩潰,如下圖所示

類似野指針的崩潰

崩潰的原因是蘸吓,由于第一次注冊KVO觀察者后沒有移除,再次進入界面宪萄,會導致第二次注冊KVO觀察者,導致KVO觀察的重復注冊居凶,而且第一次的通知對象還在內(nèi)存中侠碧,沒有進行釋放,此時接收到屬性值變化的通知谷暮,會出現(xiàn)找不到原有的通知對象,只能找到現(xiàn)有的通知對象颊埃,即第二次KVO注冊的觀察者班利,所以導致了類似野指針的崩潰,即一直保持著一個野通知闯割,且一直在監(jiān)聽

注:這里的崩潰案例是通過單例對象實現(xiàn)(崩潰有很大的幾率,不是每次必現(xiàn)),因為單例對象在內(nèi)存是常駐的御板,針對一般的類對象缎谷,貌似不移除也是可以的瑞你,但是為了防止線上意外,建議還是移除比較好

4鲫懒、KVO的自動觸發(fā)與手動觸發(fā)

KVO觀察的開啟和關閉有兩種方式甲献,自動手動

  • 自動開關,返回NO球及,就監(jiān)聽不到,返回YES际歼,表示監(jiān)聽
// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
  • 自動開關關閉的時候,可以通過手動開關監(jiān)聽
- (void)setName:(NSString *)name{
    //手動開關
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

使用手動開關的好處就是你想監(jiān)聽就監(jiān)聽旭愧,不想監(jiān)聽關閉即可,比自動觸發(fā)更方便靈活

5桃熄、KVO觀察:一對多

KVO觀察中的一對多,意思是通過注冊一個KVO觀察者,可以監(jiān)聽多個屬性的變化

以下載進度為例界弧,比如目前有一個需求划栓,需要根據(jù)總的下載量totalData當前下載量currentData 來計算當前的下載進度currentProcess,實現(xiàn)有兩種方式

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

  • 實現(xiàn)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欠母、觸發(fā)屬性值變化
- (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觀察 可變數(shù)組

KVO是基于KVC基礎之上的掷贾,所以可變數(shù)組如果直接添加數(shù)據(jù),是不會調(diào)用setter方法的添寺,所有對可變數(shù)組的KVO觀察下面這種方式不生效的,即直接通過[self.person.dateArray addObject:@"1"];向數(shù)組添加元素胯盯,是不會觸發(fā)kvo通知回調(diào)的

//1、注冊可變數(shù)組KVO觀察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    
//2计露、KVO回調(diào)
- (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、觸發(fā)數(shù)組添加數(shù)據(jù)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person.dateArray addObject:@"1"];
}

在KVC官方文檔中票罐,針對可變數(shù)組的集合類型蚕礼,有如下說明冀痕,即訪問集合對象需要需要通過mutableArrayValueForKey方法满哪,這樣才能將元素添加到可變數(shù)組

kvc中關于集合的操作方法

修改

將4中的代碼修改如下

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

運行結果如下华望,可以看到,元素被添加到可變數(shù)組了


元素被添加到可變數(shù)組

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

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

一般的屬性集合KVO觀察是有區(qū)別的,其kind不同嚣艇,以屬性name可變數(shù)組為例

  • 屬性kind一般是設值
  • 可變數(shù)組kind一般是插入
    屬性與集合的kind區(qū)別

KVO 底層原理探索

官方文檔說明

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


KVO原理-官方說明
  • KVO是使用isa-swizzling的技術實現(xiàn)的怎炊。

  • 顧名思義久橙,isa指針指向維護分配表的對象的類康嘉。該分派表實質(zhì)上包含指向該類實現(xiàn)的方法的指針以及其他數(shù)據(jù)纱控。

  • 當為對象的屬性注冊觀察者時情竹,將修改觀察對象的isa指針蒲赂,指向中間類而不是真實類互广。結果氢妈,isa指針的值不一定反映實例的實際類宋下。

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

代碼調(diào)試探索

1壹将、KVO只對屬性觀察

在LGPerson中有一個成員變量name屬性nickName,分別注冊KVO觀察,觸發(fā)屬性變化時胯甩,會有什么現(xiàn)象拓哟?

  • 分別為成員變量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通知觸發(fā)操作
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"KC";
     self.person->name    = @"Cooci";
}

運行結果如下


KVO只對屬性觀察

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

2、中間類

根據(jù)官方文檔所述,在注冊KVO觀察者后仑荐,觀察對象的isa指針指向會發(fā)生改變

  • 注冊觀察者之前:實例對象personisa指針指向LGPerson
    注冊觀察者之前
  • 注冊觀察者之后:實例對象personisa指針指向NSKVONotifying_LGPerson
    注冊觀察者之后

綜上所述,在注冊觀察者后二汛,實例對象的isa指針指向由LGPerson類變?yōu)榱?code>NSKVONotifying_LGPerson中間類匪蝙,即實例對象的isa指針指向發(fā)生了變化

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

那么這個動態(tài)生成的中間類NSKVONotifying_LGPersonLGPerson類 有什么關系?下面通過代碼來驗證

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

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個數(shù)組苫昌, 其中包含給定對象
    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);
}

//********調(diào)用********
[self printClasses:[LGPerson class]];

打印結果如下所示


中間類是子類

從結果中可以說明NSKVONotifying_LGPersonLGPerson的子類

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);
}

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

輸出結果如下


中間類方法打印

從結果中可以看出有四個方法祟身,分別是setNickName 、 class 物独、 dealloc 袜硫、 _isKVOA,這些方法是繼承還是重寫挡篓?

  • LGStudent中重寫setNickName方法婉陷,獲取LGStudent類的所有方法
    LGStudent方法打印

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

  • 獲取LGPersonNSKVONotifying_LGPerson的方法列表進行對比
    對比

綜上所述秽澳,有如下結論:

  • NSKVONotifying_LGPerson中間類重寫父類LGPersonsetNickName方法
    • NSKVONotifying_LGPerson中間類重寫基類NSObjectclass 闯睹、 dealloc 、 _isKVOA方法
      • 其中dealloc是釋放方法
      • _isKVOA判斷當前是否是kvo類

2-3担神、dealloc中移除觀察者后楼吃,isa指向是誰,以及中間類是否會銷毀妄讯?

  • 移除觀察者之前:實例對象的isa指向仍是NSKVONotifying_LGPerson中間類
    移除觀察者之前
  • 移除觀察者之后:實例對象的isa指向更改為LGPerson
    移除觀察者之后

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

那么中間類從創(chuàng)建后亥贸,到dealloc方法中移除觀察者之后躬窜,是否還存在?

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

通過子類的打印結果可以看出斩披,中間類一旦生成,沒有移除讹俊,沒有銷毀垦沉,還在內(nèi)存中 -- 主要是考慮重用的想法,即中間類注冊到內(nèi)存中仍劈,為了考慮后續(xù)的重用問題厕倍,所以中間類一直存在

總結

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

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

  • 中間類重寫了觀察屬性的setter方法class这溅、dealloc组民、_isKVOA方法

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

  • 中間類從創(chuàng)建后臭胜,就一直存在內(nèi)存中,不會被銷毀

自定義KVO

自定KVO的流程癞尚,跟系統(tǒng)一致耸三,只是在系統(tǒng)的基礎上針對其中的部分做了一些優(yōu)化處理。

  • 1浇揩、將注冊和響應通過函數(shù)式編程仪壮,即block的方法結合在一起
  • 2、去掉系統(tǒng)繁瑣的三部曲胳徽,實現(xiàn)KVO自動銷毀機制

在系統(tǒng)中积锅,注冊觀察者和KVO響應屬于響應式編程爽彤,是分開寫的,在自定義為了代碼更好的協(xié)調(diào)乏沸,使用block的形式淫茵,將注冊和回調(diào)的邏輯組合在一起,即采用函數(shù)式編程方式蹬跃,還是分為三部分

  • 注冊觀察者
//*********定義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;

準備條件:創(chuàng)建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、動態(tài)生成子類翁都,將需要重寫的class方法添加到中間類中
#pragma mark - 動態(tài)生成子類
- (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方法,為了與系統(tǒng)類對外保持一致
Class cjl_class(id self, SEL _cmd){
    //在外界調(diào)用class返回CJLPerson類
    return class_getSuperclass(object_getClass(self));//通過[self class]獲取會造成死循環(huán)
}
  • 3柄慰、isa指向由原有類平挑,改為指向中間類
object_setClass(self, newClass);
  • 4魔种、保存信息:這里用的數(shù)組,也可以使用map,需要創(chuàng)建信息的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];
//使用數(shù)組存儲 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
if (!mArray) {//如果mArray不存在亡驰,則重新創(chuàng)建
    mArray = [NSMutableArray arrayWithCapacity:1];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];

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

#pragma mark - 注冊觀察者 - 函數(shù)式編程
- (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];
    //使用數(shù)組存儲 -- 也可以使用map
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey));
    if (!mArray) {//如果mArray不存在细层,則重新創(chuàng)建
        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儒鹿、動態(tài)生成子類、
    /*
        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
    設置
  • class方法必須重寫业踏,其目的是為了與系統(tǒng)一樣,對外的類保持一致涧卵,如下所示
    • 系統(tǒng)的KVO勤家,在添加觀察者前后,實例對象person的類一直都是CJLPerson

      系統(tǒng)KVO-注冊前后對比

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

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

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

KVO響應

主要是給子類動態(tài)添加setter方法,其目的是為了在setter方法中向父類發(fā)送消息断凶,告知其屬性值的變化

  • 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、通過將系統(tǒng)的objc_msgSendSuper強制類型轉換自定義的消息發(fā)送cjl_msgSendSuper
//往父類LGPerson發(fā)消息 - 通過objc_msgSendSuper
//通過系統(tǒng)強制類型轉換自定義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)), //第一次快捷查找的類 為 父類
};
//調(diào)用自定義的發(fā)送消息函數(shù)
cjl_msgSendSuper(&superStruct, _cmd, newValue);
  • 7认烁、告知vc去響應:獲取信息肿男,通過block傳遞
/*---函數(shù)式編程*/
    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發(fā)消息 - 通過objc_msgSendSuper
    //通過系統(tǒng)強制類型轉換自定義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)), //第一次快捷查找的類 為 父類
    };
    //調(diào)用自定義的發(fā)送消息函數(shù)
    cjl_msgSendSuper(&superStruct, _cmd, newValue);
    
    //此時應該有didChange的代碼
    
    //讓vc去響應
    /*---函數(shù)式編程*/
    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);
        }
    }
}

移除觀察者

為了避免在外界不斷的調(diào)用removeObserver方法介汹,在自定義KVO中實現(xiàn)自動移除觀察者

  • 8、實現(xiàn)cjl_removeObserver:forKeyPath:方法舶沛,主要是清空數(shù)組嘹承,以及isa指向更改
- (void)cjl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    //清空數(shù)組
    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方法如庭,當子類銷毀時叹卷,會自動調(diào)用dealloc方法(在動態(tài)生成子類的方法中添加)
#pragma mark - 動態(tài)生成子類
- (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發(fā)送消息釋放即dealloc了,就會自動走到重寫的cjl_dealloc方法中(原因是因為person對象的isa指向變了坪它,指向中間類骤竹,但是實例對象的地址是不變的,所以子類的釋放往毡,相當于釋放了外界的person蒙揣,而重寫的cjl_dealloc相當于是重寫了CJLPerson的dealloc方法,所以會走到cjl_dealloc方法中)开瞭,達到自動移除觀察者的目的

總結

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

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

    • 2嗤详、保存信息

    • 3个扰、動態(tài)生成子類,需要重寫class断楷、setter方法

    • 4锨匆、在子類的setter方法中向父類發(fā)消息,即自定義消息發(fā)送

    • 5冬筒、讓觀察者響應

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

    • 2、重寫子類的dealloc方法

拓展

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

自定義KVO的完整代碼見Github-CustomKVC_KVO,喜歡的可以點個?

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末响牛,一起剝皮案震驚了整個濱河市玷禽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呀打,老刑警劉巖矢赁,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異贬丛,居然都是意外死亡撩银,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門豺憔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來额获,“玉大人够庙,你說我怎么就攤上這事〕” “怎么了耘眨?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長境肾。 經(jīng)常有香客問我剔难,道長,這世上最難降的妖魔是什么准夷? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任钥飞,我火速辦了婚禮,結果婚禮上衫嵌,老公的妹妹穿的比我還像新娘读宙。我一直安慰自己,他們只是感情好楔绞,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布结闸。 她就那樣靜靜地躺著,像睡著了一般酒朵。 火紅的嫁衣襯著肌膚如雪桦锄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天蔫耽,我揣著相機與錄音结耀,去河邊找鬼。 笑死匙铡,一個胖子當著我的面吹牛图甜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鳖眼,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼黑毅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钦讳?” 一聲冷哼從身側響起矿瘦,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎愿卒,沒想到半個月后缚去,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡琼开,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年病游,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡衬衬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出改橘,到底是詐尸還是另有隱情滋尉,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布飞主,位于F島的核電站狮惜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏碌识。R本人自食惡果不足惜碾篡,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筏餐。 院中可真熱鬧开泽,春花似錦、人聲如沸魁瞪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽导俘。三九已至峦耘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旅薄,已是汗流浹背辅髓。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留少梁,地道東北人洛口。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像猎莲,于是被迫代替她去往敵國和親绍弟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355