iOS-KVO

一适袜、VKO 簡(jiǎn)述

KVO 全稱(chēng) Key Value Observing趁尼,俗稱(chēng)“鍵值監(jiān)聽(tīng)”;可以監(jiān)聽(tīng)對(duì)象某個(gè)屬性值的變化

1. KVO 是已什么方式實(shí)現(xiàn)的嚎尤?(底層原理是什么荔仁?)

答:當(dāng)對(duì)一個(gè)對(duì)象添加監(jiān)聽(tīng)(addObserver:forKeyPath: ... ),iOS會(huì)修改該對(duì)象的 isa (isa默認(rèn)指向?qū)ο笏鶎俚念?lèi))芽死。改為指向一個(gè)通過(guò)Runtime動(dòng)態(tài)創(chuàng)建的子類(lèi)乏梁,子類(lèi)擁重寫(xiě) set 方法,并且 set 方法內(nèi)部會(huì)順序調(diào)用 willChangeValueForKey, 原來(lái)的set方法,即:[super set...], didChangeValueForKey关贵。并且會(huì)在 didChangeValueForKey 中調(diào)用KVO的回調(diào)方法:observeValueForKeyPath:ofObject:change:context:

2. 如何手動(dòng)觸發(fā)KVO?

答:已添加監(jiān)聽(tīng)的屬性遇骑,在值發(fā)生變化時(shí),系統(tǒng)會(huì)自動(dòng)觸發(fā)回調(diào)揖曾。如果想要手動(dòng)觸發(fā)落萎,則需我們自己調(diào)用 willChangeValueFor 和 didChallengeValueForKey方法,這兩個(gè)方法缺一不可炭剪。

二练链、KVO 實(shí)現(xiàn)原理探索

1. 話不多說(shuō),上代碼:
- (void)useSystemKVOTest {
    // 1. 創(chuàng)建測(cè)試對(duì)象
    self.p1 = [Person new];
    self.p2 = [Person new];
    self.p1.age = 1;
    self.p2.age = 2;

    // 2. 打印監(jiān)聽(tīng)前p1奴拦、p2 所屬類(lèi)媒鼓、setter 方法實(shí)現(xiàn)地址
    NSLog(@"監(jiān)聽(tīng)前 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
    // 輸出結(jié)果:監(jiān)聽(tīng)前 p1 class is : Person, p2 class is : Person
    NSLog(@"監(jiān)聽(tīng)前 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
    // 輸出結(jié)果:監(jiān)聽(tīng)前 p1-setAage: address is : = 0x102f98ea8, p2-setAage: address is : 0x102f98ea8

    // 3. 添加監(jiān)聽(tīng),
    [self.p1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

    // 4. 打印監(jiān)聽(tīng)后p1粱坤、p2 所屬類(lèi)隶糕、setter 方法實(shí)現(xiàn)地址
    NSLog(@"監(jiān)聽(tīng)后 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
    // 輸出結(jié)果:監(jiān)聽(tīng)后 p1 class is : NSKVONotifying_Person, p2 class is : Person
    NSLog(@"監(jiān)聽(tīng)后 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
    // 輸出結(jié)果:監(jiān)聽(tīng)后 p1-setAage: address is : = 0x194c61d54, p2-setAage: address is : 0x102f98ea8
    
    // 5. 改變值
    self.p1.age = 10;
    self.p2.age = 20;

    // 6.移除 p1.age 的監(jiān)聽(tīng)者
    [self.p1 removeObserver:self forKeyPath:NSStringFromSelector(@selector(age))];
}

// kvo 回調(diào)方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"監(jiān)聽(tīng)到 %@ 的 %@ 改變了 %@", [object isEqual:self.p1]?@"p1":@"p2", keyPath, change);
    /* 輸出結(jié)果:
     監(jiān)聽(tīng)到 p1 的 age 改變了 {
         kind = 1;
         new = 10;
         old = 1;
     }
     */
}
輸出結(jié)果:001
2. 有以上輸出結(jié)果,我們發(fā)現(xiàn):
  • 在添加監(jiān)聽(tīng)后站玄,p1 的 isa 指向了 NSKVONotifying_Person
  • NSKVONotifyin_Person其實(shí)是Person的子類(lèi),那么也就是說(shuō)其superclass指針是指向Person類(lèi)對(duì)象的
  • NSKVONotifyin_Person 是 runtime 在運(yùn)行時(shí)生成的濒旦。那么 p1 對(duì)象在調(diào)用 setage 方法的時(shí)候株旷,肯定會(huì)根據(jù) p1 的 isa 找到NSKVONotifyin_Person,在 NSKVONotifyin_Person 中找 setage 的方法及實(shí)現(xiàn)。
  • p1 的 setAge 方法的實(shí)現(xiàn)由 Person 類(lèi)方法中的 setAge 方法轉(zhuǎn)換為了C語(yǔ)言的 Foundation 框架的 _NSsetIntValueAndNotify 函數(shù)晾剖。
3. NSKVONotifyin_Person 的內(nèi)部結(jié)構(gòu):

首先我們知道锉矢,NSKVONotifyin_Person作為Person的子類(lèi),其superclass指針指向Person類(lèi)齿尽,并且NSKVONotifyin_Person內(nèi)部一定對(duì)setAge方法做了單獨(dú)的實(shí)現(xiàn)沽损,那么NSKVONotifyin_Person同Person類(lèi)的差別可能就在于其內(nèi)存儲(chǔ)的對(duì)象方法及實(shí)現(xiàn)不同。
我們通過(guò)runtime分別打印Person類(lèi)對(duì)象和NSKVONotifyin_Person類(lèi)對(duì)象內(nèi)存儲(chǔ)的對(duì)象方法

- (void)printMethods {
    [self printMehtodsOfClass:object_getClass(self.p1)];
    [self printMehtodsOfClass:object_getClass(self.p2)];
}

- (void)printMehtodsOfClass:(Class)cls {
    unsigned int count = 0;
    Method * methods = class_copyMethodList(cls, &count);    
    NSMutableString *methodNames = @"".mutableCopy;
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString * methodName = NSStringFromSelector(method_getName(method));
        [methodNames appendString:methodName];
        [methodNames appendString:@"  "];
    }    
    NSLog(@"%@", methodNames);
    free(methods);
}
輸出結(jié)果002

通過(guò)上述代碼我們發(fā)現(xiàn)NSKVONotifyin_Person中有4個(gè)對(duì)象方法循头。分別為setAge: class dealloc _isKVOA绵估,那么至此我們可以畫(huà)出NSKVONotifyin_Person的內(nèi)存結(jié)構(gòu)以及方法調(diào)用順序。

image.png

這里NSKVONotifyin_Person重寫(xiě)class方法是為了隱藏NSKVONotifyin_Person卡骂。不被外界所看到国裳。我們?cè)趐1添加過(guò)KVO監(jiān)聽(tīng)之后,分別打印p1和p2對(duì)象的class可以發(fā)現(xiàn)他們都返回Person全跨。

NSLog(@"%@, %@", [self.p1 class],  [self.p2 class]);
// 打印結(jié)果 Person, Person

三. 自定義 KVO 實(shí)現(xiàn)監(jiān)聽(tīng)

1. ViewController 調(diào)用實(shí)現(xiàn):
#import "ViewController.h"
#import "Person.h"
#import "NSObject+YJKVO.h"

@interface ViewController ()
@property (nonatomic, strong) Person * p;
@end

@implementation ViewController
#pragma mark - Life Cycle
- (void)viewDidLoad {
    [super viewDidLoad];
    [self useCustomKVOTest];
}

#pragma mark - 使用自定義kvo
- (void)useCustomKVOTest {
    self.p = [[Person alloc] init];
    [self.p yj_addObserver:self forKeyPath:NSStringFromSelector(@selector(name))];
    self.p.name = @"張三";
}

#pragma mark - 自定義kvo缝左,回調(diào)
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue {
    NSLog(@"newValue = %@", newValue);
}
2. Person 類(lèi)
  • Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end
  • Person.m
#import "Person.h"

@implementation Person
- (void)setName:(NSString *)name {
    _name = name;
    NSLog(@"調(diào)用了");
}
@end
3. 定義一個(gè) NSObject 的分類(lèi) NSObject+YJKVO,實(shí)現(xiàn)KVO監(jiān)聽(tīng)
  • NSObject+YJKVO.h
@interface NSObject (YJKVO)
/// 添加觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

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

/// kvo 回調(diào)方法 (由觀察者實(shí)現(xiàn))
/// @param keyPath keyPath
/// @param object 被觀察對(duì)象
/// @param newValue 新值
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue;
@end
  • NSObject+YJKVO.m
#import "NSObject+YJKVO.h"
#import <objc/message.h>

// 通過(guò) Runtime 動(dòng)態(tài)成子類(lèi)的前綴
static NSString *const YJKVOPrefix = @"YJKVO_";
// 關(guān)聯(lián) 觀察者
static NSString *const YJKVOAssociatedOberverKey = @"YJKVOAssociatedOberverKey";

@implementation NSObject (YJKVO)

#pragma mark - -- public methods
/// 添加觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {    
    // 1. 檢查時(shí)候有 set 方法
    NSString *setterMethodName = setterForGetter(keyPath);
    SEL setterSel = NSSelectorFromString(setterMethodName);
    // method
    Method method = class_getInstanceMethod(self.class, setterSel);
    if (!method) {
        @throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"沒(méi)有setter方法" userInfo:nil];
    }
    
    // 2. 動(dòng)態(tài)生成子類(lèi)
    Class sub_Class = [self registerSubClassWithKeyPath:keyPath];
    if (!sub_Class) {
        @throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"子類(lèi)創(chuàng)建失敗" userInfo:nil];
    }
    
    // 3. 消息轉(zhuǎn)發(fā)
    // 關(guān)聯(lián) observer
    objc_setAssociatedObject(self, (__bridge void const * _Nonnull)YJKVOAssociatedOberverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

/// 移除觀察者
/// @param observer 觀察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    objc_removeAssociatedObjects(observer);
}

/// kvo 回調(diào)方法 (由觀察者實(shí)現(xiàn))
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(nonnull id)object newValue:(nonnull id)newValue { }


#pragma mark - -- private methods
#pragma mark - 通過(guò) getter 方法名浓若,獲取 setter 方法名渺杉;例如:age ==> setAge:
static NSString * setterForGetter(NSString *getter) {
    if (getter.length < 1) {
        return nil;
    }
    // 獲取第一個(gè)字符,變成打下
    NSString *firstString = [[getter substringToIndex:1] uppercaseString]; // substringToIndex:從最前頭一直截取到Index
    NSString *otherString = [getter substringFromIndex:1]; // substringFromIndex:從Index開(kāi)始截取到最后
    // 拼接 age == > setAag:
    return [NSString stringWithFormat:@"set%@%@:", firstString, otherString];
}

#pragma mark - 通過(guò) setter 方法名挪钓,獲取 getter 方法名是越;例如:setAge: ==> age
static NSString * getterForSetter(NSString *setter) {
    if (setter.length < 1 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
        return nil;
    }
    NSString *getter = [setter substringFromIndex:3];
    getter = [getter substringToIndex:getter.length-1];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}


#pragma mark - 動(dòng)態(tài)生成子類(lèi)
/// 運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建子類(lèi)
/// @param keyPath keyPath
- (Class)registerSubClassWithKeyPath:(NSString *)keyPath {
    // 子類(lèi)名
    NSString *subClsName = [NSString stringWithFormat:@"%@%@", YJKVOPrefix, self.class];
    // 子類(lèi),一個(gè) NSObject 默認(rèn)分貝 16 個(gè)字節(jié)
    Class subCls = objc_allocateClassPair(self.class, subClsName.UTF8String, 16);
    // 注冊(cè)
    objc_registerClassPair(subCls);
    
    // 給子類(lèi)動(dòng)態(tài)添加 setter诵原、class 實(shí)現(xiàn)
    Method class_method = class_getClassMethod(self.class, @selector(class));
    Method setter_method = class_getClassMethod(self.class, NSSelectorFromString(setterForGetter(keyPath)));
    class_addMethod(subCls, @selector(class), (IMP)yj_class, method_getTypeEncoding(class_method));
        class_addMethod(subCls,  NSSelectorFromString(setterForGetter(keyPath)), (IMP)yj_setter, method_getTypeEncoding(setter_method));
    
    // 將父類(lèi)的 isa 指向子類(lèi)
    object_setClass(self, subCls);
    // 返回
    return subCls;
}

#pragma mark - 重寫(xiě) class 方法
static Class yj_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - 重寫(xiě) setter 方法
/// 重寫(xiě) setter 方法
/// @param newValue 新值
static void yj_setter(id self, SEL _cmd, id newValue) {    
    // 1. 調(diào)用 super setter 方法
    struct objc_super super_cls = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 調(diào)用父類(lèi) setter 方法 設(shè)置新值
    ((void(*) (id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&super_cls), _cmd, newValue);
            
    // 2. 取出觀察者英妓,調(diào)用kvo 回調(diào)方法
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(YJKVOAssociatedOberverKey));
    //
    SEL handleSel = @selector(yj_observeValueForKeyPath:ofObject:newValue:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    
    // Runtime 調(diào)用回到方法
    // objc_msgSend() 默認(rèn)的情況下,不支持添加參數(shù)绍赛。
    // 解決方案一: Build Setting –> 搜索: Enable Strict Checking of objc_msgSend Calls 改為 NO (我自己試了下蔓纠,無(wú)效 Xcode12.1)
    // 解決方案二: 這里通過(guò)(void *)送入5個(gè)參數(shù),你可以根據(jù)自己參數(shù)類(lèi)型強(qiáng)轉(zhuǎn)原本是void()的函數(shù)方法
    ((void (*) (id, SEL, NSString*, id, id)) (void*)objc_msgSend)(observer, handleSel, keyPath, self, newValue);
}
@end
輸出結(jié)果003
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吗蚌,一起剝皮案震驚了整個(gè)濱河市腿倚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚯妇,老刑警劉巖敷燎,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異箩言,居然都是意外死亡硬贯,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)陨收,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)饭豹,“玉大人鸵赖,你說(shuō)我怎么就攤上這事≈羲ィ” “怎么了它褪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)翘悉。 經(jīng)常有香客問(wèn)我茫打,道長(zhǎng),這世上最難降的妖魔是什么妖混? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任老赤,我火速辦了婚禮,結(jié)果婚禮上源葫,老公的妹妹穿的比我還像新娘诗越。我一直安慰自己,他們只是感情好息堂,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布嚷狞。 她就那樣靜靜地躺著,像睡著了一般荣堰。 火紅的嫁衣襯著肌膚如雪床未。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,328評(píng)論 1 310
  • 那天振坚,我揣著相機(jī)與錄音薇搁,去河邊找鬼。 笑死渡八,一個(gè)胖子當(dāng)著我的面吹牛啃洋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屎鳍,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宏娄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了逮壁?” 一聲冷哼從身側(cè)響起孵坚,我...
    開(kāi)封第一講書(shū)人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窥淆,沒(méi)想到半個(gè)月后卖宠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忧饭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年扛伍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片词裤。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜒秤,死狀恐怖汁咏,靈堂內(nèi)的尸體忽然破棺而出亚斋,到底是詐尸還是另有隱情作媚,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布帅刊,位于F島的核電站纸泡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏赖瞒。R本人自食惡果不足惜女揭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栏饮。 院中可真熱鬧吧兔,春花似錦、人聲如沸袍嬉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)伺通。三九已至箍土,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間罐监,已是汗流浹背吴藻。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留弓柱,地道東北人沟堡。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像矢空,于是被迫代替她去往敵國(guó)和親航罗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359