kvo整理

1.概述

KVO,即:Key-Value Observing淮椰,是 Objective-C 對 觀察者模式(Observer Pattern)的實現(xiàn)蟹漓。它提供一種機制栏赴,當(dāng)指定的對象的屬性被修改后镊辕,觀察者就會接受到通知痛阻。簡單的說就是每次指定的被觀察的對象的屬性被修改后菌瘪,KVO就會自動通知相應(yīng)的觀察者了。

2.使用

1. 基本使用

KVO本質(zhì)上是基于runtime的動態(tài)分發(fā)機制,通過key來監(jiān)聽value的值俏扩。
OC能夠?qū)崿F(xiàn)監(jiān)聽因為都遵守了NSKeyValueCoding協(xié)議糜工。OC所有的類都是繼承自NSObject,其默認(rèn)已經(jīng)遵守了該協(xié)議录淡,但Swift不是基于runtime的捌木。Swift中繼承自NSObject的屬性處于性能等方面的考慮,默認(rèn)是關(guān)閉動態(tài)分發(fā)的嫉戚, 所以無法使用KVO刨裆,只有在屬性前加 @objc dynamic 才會開啟運行時,允許監(jiān)聽屬性的變化彬檀。

在Swift3中只需要加上dynamic就可以了帆啃,而Swift4以后則還需要@objc

  • 注冊
- (void)addObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(void *)context;

observer:觀察者,也就是KVO通知的訂閱者窍帝。訂閱著必須實現(xiàn)努潘。

keyPath:描述將要觀察的屬性,相對于被觀察者盯桦。

options:KVO的一些屬性配置慈俯;有四個選項渤刃。

NSKeyValueObservingOptionNew:change字典包括改變后的值
NSKeyValueObservingOptionOld:change字典包括改變前的值
NSKeyValueObservingOptionInitial:注冊后立刻觸發(fā)KVO通知
NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變后通知兩次)
  • 監(jiān)聽
    在觀察者內(nèi)重寫這個方法拥峦。在屬性變化時,觀察者則可以在函數(shù)內(nèi)對屬性變化做處理卖子。
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
  • 移除

在不用的時候略号,不要忘記解除注冊,否則會導(dǎo)致內(nèi)存泄露洋闽。

- (void)removeObserver:(NSObject *)observer 
                forKeyPath:(NSString *)keyPath;
  • 舉例
class ObservedClass: NSObject {
    // 開啟運行時玄柠,允許監(jiān)聽屬性的變化
    @objc dynamic var name: String = "Original"
    // age 并不會觸發(fā)KVO
    var age: Int = 18
}

class ViewController: UIViewController {
    var observed = ObservedClass()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        observed.addObserver(self, forKeyPath: "age", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        // 修改屬性值,觸發(fā)KVO
        observed.name = "JiangT"
        observed.age = 22
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("屬性改變了")
        print(keyPath)
        print("change字典為:")
        print(change)
    }
}

---輸出結(jié)果---
屬性改變了
Optional("name")
change字典為:
Optional(
[__C.NSKeyValueChangeKey(_rawValue: new): JiangT, 
__C.NSKeyValueChangeKey(_rawValue: kind): 1, 
__C.NSKeyValueChangeKey(_rawValue: old): Original])
2. 手動KVO 及禁用KVO
  1. 首先诫舅,需要手動實現(xiàn)屬性的 setter 方法羽利,并在設(shè)置操作的前后分別調(diào)用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用于通知系統(tǒng)該 key 的屬性值即將和已經(jīng)變更了刊懈。
  2. 其次这弧,要實現(xiàn)類方法 automaticallyNotifiesObserversForKey,并在其中設(shè)置對該 key 不自動發(fā)送通知(返回 NO 即可)虚汛。這里要注意匾浪,對其它非手動實現(xiàn)的 key,要轉(zhuǎn)交給 super 來處理卷哩。
  3. 如果需要禁用該類KVO的話直接automaticallyNotifiesObserversForKey返回NO蛋辈,實現(xiàn)屬性的 setter 方法,不進(jìn)行調(diào)用willChangeValueForKey: 和 didChangeValueForKey方法将谊。

主要方法:

open func willChangeValue(forKey key: String)

open func didChangeValue(forKey key: String)

class func automaticallyNotifiesObservers(forKey key: String) -> Bool

舉例

---被觀察類---
class ObservedClass: NSObject {
 
    private var _name: String = "Original"
    @objc dynamic var name: String {
        get {
            return _name
        }
        set (n) {
            self.willChangeValue(forKey: "name")
            _name = n
            self.didChangeValue(forKey: "name")
        }
    }
    
    override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
        // 設(shè)置對該 key 不自動發(fā)送通知
        if key == "name" {
            return false
        }
        return super.automaticallyNotifiesObservers(forKey: key)
    }
}

class ViewController: UIViewController {
    var observed = ObservedClass()
    override func viewDidLoad() {
        super.viewDidLoad()
        
        observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
        // 修改屬性值冷溶,觸發(fā)KVO
        observed.name = "JiangT"
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("屬性改變了")
        print(keyPath)
        print("change字典為:")
        print(change)
    }
}

---輸出結(jié)果---
屬性改變了
Optional("name")
change字典為:
Optional([__C.NSKeyValueChangeKey(_rawValue: kind): 1, 
__C.NSKeyValueChangeKey(_rawValue: old): Original, 
__C.NSKeyValueChangeKey(_rawValue: new): JiangT])

3. 實現(xiàn)原理

Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

大致意思為:
蘋果使用了一種isa交換的技術(shù)渐白,當(dāng)ObjectA的被觀察后,ObjectA對象的isa指針被指向了一個新建的子類NSKVONotifying_ObjectA逞频,且這個子類重寫了被觀察值的setter方法和class方法礼预,dealloc和isKVO方法,然后使ObjectA對象的isa指針指向這個新建的類虏劲,然后事實上ObjectA變?yōu)榱薔SKVONotifying ObjectA的實例對象托酸,執(zhí)行方法要從這個類的方法列表里找。

  • KVO是基于runtime機制實現(xiàn)的柒巫。

  • 當(dāng)某個類的屬性對象第一次被觀察時励堡,系統(tǒng)就會在運行期動態(tài)地創(chuàng)建該類的一個派生類(如果原類為ObservedClass,那么生成的派生類名為NSKVONotifying_ObservedClass)堡掏,在這個派生類中重寫基類中任何被觀察屬性的setter方法应结。派生類在被重寫的setter方法內(nèi)實現(xiàn)真正的通知機制

  • 每個類對象中都有一個isa指針指向當(dāng)前類,當(dāng)一個類對象的第一次被觀察泉唁,那么系統(tǒng)會偷偷將isa指針指向動態(tài)生成的派生類(isa-swizzling鹅龄,后續(xù)Runtime學(xué)習(xí)記錄中展開),從而在給被監(jiān)控屬性賦值時執(zhí)行的是派生類的setter方法亭畜。派生類中還偷偷重寫了class方法扮休,讓我們誤認(rèn)為還是使用的當(dāng)前類,從而達(dá)到隱藏生成的派生類拴鸵。
測試代碼
@interface KVOObject : NSObject
@property (nonatomic, copy  ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation KVOObject

- (NSString *)description {
    NSLog(@"object address : %p \n", self);
    
    IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
    IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
    NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
    
    Class objectMethodClass = [self class];
    Class objectRuntimeClass = object_getClass(self);
    Class superClass = class_getSuperclass(objectRuntimeClass);
    NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
    
    NSLog(@"object method list \n");
    unsigned int count;
    Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"method Name = %@\n", methodName);
    }
    
    return @"";
}

在另一個類中分別創(chuàng)建兩個KVOObject對象玷坠,其中一個對象被觀察者通過KVO的方式監(jiān)聽,另一個對象則始終沒有被監(jiān)聽劲藐。在KVO前后分別打印兩個對象的關(guān)鍵信息八堡,看KVO前后有什么變化。

@property (nonatomic, strong) KVOObject *object1;
@property (nonatomic, strong) KVOObject *object2;

self.object1 = [[KVOObject alloc] init];
self.object2 = [[KVOObject alloc] init];
[self.object1 description];
[self.object2 description];

[self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

[self.object1 description];
[self.object2 description];

self.object1.name = @"lxz";
self.object1.age = 20;

下面是KVO前后的打印信息

// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA

object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age

我們發(fā)現(xiàn)對象被KVO后聘芜,其真正類型變?yōu)榱薔SKVONotifying_KVOObject類兄渺,已經(jīng)不是之前的類了。KVO會在運行時動態(tài)創(chuàng)建一個新類汰现,將對象的isa指向新創(chuàng)建的類挂谍,新類是原類的子類,命名規(guī)則是NSKVONotifying_xxx的格式服鹅。KVO為了使其更像之前的類凳兵,還會將對象的class實例方法重寫,使其更像原類企软。

自定義kvo實現(xiàn)

static void *const zs_KVOObserverAssociatedKey = (void *)&zs_KVOObserverAssociatedKey;
static NSString *zs_KVOClassPrefix = @"zs_KVONotifying_";
@implementation KVOObserverItem

- (instancetype)initWithObserver:(NSObject *)observer
                             key:(NSString *)key
                           block:(zs_KVOObserverBlock)block {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.key = key;
        self.block = block;
    }
    return self;
}

@end


- (void)zs_addObserver:(NSObject *)observer
       keyPath:(NSString *)keyPath
               callback:(zs_KVOObserverBlock)callback {
    
    // 1. 通過Method判斷是否有這個key對應(yīng)的selector庐扫,如果沒有則Crash。
    SEL originalSetter = NSSelectorFromString(zs_setterForGetter(keyPath));
    Method originalMethod = class_getInstanceMethod(object_getClass(self), originalSetter);
    if (!originalMethod) {
        NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
        NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
        [exception raise];
    }
    
    // 2. 判斷當(dāng)前類是否是KVO子類,如果不是則創(chuàng)建形庭,并設(shè)置其isa指針铅辞。
    Class kvoClass = object_getClass(self);
    NSString *kvoClassString = NSStringFromClass(kvoClass);
    if (![kvoClassString hasPrefix:zs_KVOClassPrefix]) {
        kvoClass = [self zs_makeKVOClassWithName:kvoClassString];
        object_setClass(self, kvoClass);
    }
    
    // 3. 如果沒有實現(xiàn),則添加Key對應(yīng)的setter方法萨醒。
    if (![self zs_hasMethodWithKey:originalSetter]) {
        class_addMethod(kvoClass, originalSetter, (IMP)zs_kvoSetter, method_getTypeEncoding(originalMethod));
    }
    
    // 4. 將調(diào)用對象添加到數(shù)組中斟珊。
    KVOObserverItem *observerItem = [[KVOObserverItem alloc] initWithObserver:observer key:keyPath block:callback];
    NSMutableArray<KVOObserverItem *> *observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
    if (observers == nil) {
        observers = [NSMutableArray array];
    }
    [observers addObject:observerItem];
    objc_setAssociatedObject(self, zs_KVOObserverAssociatedKey, observers, OBJC_ASSOCIATION_RETAIN);
}

- (void)zs_removeObserver:(NSObject *)observer
          keyPath:(NSString *)keyPath {
    NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
    [observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
        if (mapTable.observer == observer && keyPath == mapTable.key) {
            [observers removeObject:mapTable];
        }
    }];
}

#pragma mark - ----- Private Method Or Funcation ------

static void zs_kvoSetter(id self, SEL selector, id value) {
    // 1. 獲取舊值。
    id (*getterMsgSend) (id, SEL) = (void *)objc_msgSend;
    NSString *getterString = zs_getterForSetter(selector);
    SEL getterSelector = NSSelectorFromString(getterString);
    id oldValue = getterMsgSend(self, getterSelector);
    
    // 2. 創(chuàng)建super的結(jié)構(gòu)體富纸,并向super發(fā)送屬性的消息囤踩。
    id (*msgSendSuper) (void *, SEL, id) = (void *)objc_msgSendSuper;
    struct objc_super objcSuper = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    msgSendSuper(&objcSuper, selector, value);
    
    // 3. 遍歷調(diào)用block。
    NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
    [observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([mapTable.key isEqualToString:getterString] && mapTable.block) {
            mapTable.block(self, NSStringFromSelector(selector), oldValue, value);
        }
    }];
}

- (BOOL)zs_hasMethodWithKey:(SEL)key {
    NSString *setterName = NSStringFromSelector(key);
    unsigned int count;
    Method *methodList = class_copyMethodList(object_getClass(self), &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        if ([methodName isEqualToString:setterName]) {
            return YES;
        }
    }
    return NO;
}

static NSString * zs_getterForSetter(SEL setter) {
    NSString *setterString = NSStringFromSelector(setter);
    if (![setterString hasPrefix:@"set"]) {
        return nil;
    }
    
    NSString *getterString = [setterString substringWithRange:NSMakeRange(4, setterString.length - 5)];
    NSString *firstString = [setterString substringWithRange:NSMakeRange(3, 1)];
    firstString = [firstString lowercaseString];
    getterString = [NSString stringWithFormat:@"%@%@", firstString, getterString];
    return getterString;
}

static NSString * zs_setterForGetter(NSString *getter) {
    NSString *getterString = getter;
    NSString *firstString = [getterString substringToIndex:1];
    firstString = [firstString uppercaseString];
    
    NSString *setterString = [getterString substringFromIndex:1];
    setterString = [NSString stringWithFormat:@"set%@%@:", firstString, setterString];
    return setterString;
}


- (Class)zs_makeKVOClassWithName:(NSString *)name {
    // 1. 判斷是否存在KVO類晓褪,如果存在則返回堵漱。
    NSString *className = [NSString stringWithFormat:@"%@%@", zs_KVOClassPrefix, name];
    Class kvoClass = objc_getClass(className.UTF8String);
    if (kvoClass) {
        return kvoClass;
    }
    
    // 2. 如果不存在,則創(chuàng)建KVO類涣仿。
    kvoClass = objc_allocateClassPair(object_getClass(self), className.UTF8String, 0);
    objc_registerClassPair(kvoClass);
    
    // 3. 重寫KVO類的class方法勤庐,指向自定義的IMP。
    Method method = class_getInstanceMethod(object_getClass(self), @selector(class));
    const char *types = method_getTypeEncoding(method);
    class_addMethod(kvoClass, @selector(class), (IMP)zs_kvoClass, types);
    
    return kvoClass;
}

static Class zs_kvoClass(id self, SEL selector) {
    return class_getSuperclass(object_getClass(self));
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末好港,一起剝皮案震驚了整個濱河市愉镰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钧汹,老刑警劉巖丈探,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異崭孤,居然都是意外死亡类嗤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門辨宠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人货裹,你說我怎么就攤上這事嗤形。” “怎么了弧圆?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵赋兵,是天一觀的道長。 經(jīng)常有香客問我搔预,道長霹期,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任拯田,我火速辦了婚禮历造,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己吭产,他們只是感情好侣监,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著臣淤,像睡著了一般橄霉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上邑蒋,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天姓蜂,我揣著相機與錄音,去河邊找鬼医吊。 笑死覆糟,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遮咖。 我是一名探鬼主播滩字,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼御吞!你這毒婦竟也來了麦箍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤陶珠,失蹤者是張志新(化名)和其女友劉穎挟裂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揍诽,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡诀蓉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了暑脆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渠啤。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖添吗,靈堂內(nèi)的尸體忽然破棺而出沥曹,到底是詐尸還是另有隱情,我是刑警寧澤碟联,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布妓美,位于F島的核電站,受9級特大地震影響鲤孵,放射性物質(zhì)發(fā)生泄漏壶栋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一普监、第九天 我趴在偏房一處隱蔽的房頂上張望贵试。 院中可真熱鬧琉兜,春花似錦、人聲如沸锡移。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淆珊。三九已至夺饲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間施符,已是汗流浹背往声。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戳吝,地道東北人浩销。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像听哭,于是被迫代替她去往敵國和親慢洋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容