KVO原理分析及使用進階

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/badf5cac0130git


咱們在工做中常常會用到KVO,可是系統原生的KVO並很差用,很容易致使Crash。並且編寫代碼時,須要編寫大量KVO相關的代碼,因爲不支持block的形式,代碼會寫的很分散。github

本篇文章對KVO的實現原理進行了詳細的分析,而且簡單的實現了一個KVO,來當作技術交流。因爲系統提供的KVO存在不少問題,在文章的最下面給出瞭解決方案。安全


博客配圖

概述

KVO全稱KeyValueObserving,是蘋果提供的一套事件通知機制。容許對象監聽另外一個對象特定屬性的改變,並在改變時接收到事件。因爲KVO的實現機制,因此對屬性纔會發生做用,通常繼承自NSObject的對象都默認支持KVO框架

KVONSNotificationCenter都是iOS中觀察者模式的一種實現。區別在於,相對於被觀察者和觀察者之間的關係,KVO是一對一的,而不一對多的。KVO對被監聽對象無侵入性,不須要修改其內部代碼便可實現監聽。函數

KVO能夠監聽單個屬性的變化,也能夠監聽集合對象的變化。經過KVCmutableArrayValueForKey:等方法得到代理對象,當代理對象的內部對象發生改變時,會回調KVO監聽的方法。集合對象包含NSArrayNSSet源碼分析

基礎使用

使用KVO分爲三個步驟:測試

  1. 經過addObserver:forKeyPath:options:context:方法註冊觀察者,觀察者能夠接收keyPath屬性的變化事件。
  2. 在觀察者中實現observeValueForKeyPath:ofObject:change:context:方法,當keyPath屬性發生改變後,KVO會回調這個方法來通知觀察者。
  3. 當觀察者不須要監聽時,能夠調用removeObserver:forKeyPath:方法將KVO移除。須要注意的是,調用removeObserver須要在觀察者消失以前,不然會致使Crash

註冊方法

在註冊觀察者時,能夠傳入options參數,參數是一個枚舉類型。若是傳入NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld表示接收新值和舊值,默認爲只接收新值。若是想在註冊觀察者後,當即接收一次回調,則能夠加入NSKeyValueObservingOptionInitial枚舉。ui

還能夠經過方法context傳入任意類型的對象,在接收消息回調的代碼中能夠接收到這個對象,是KVO中的一種傳值方式。atom

在調用addObserver方法後,KVO並不會對觀察者進行強引用,因此須要注意觀察者的生命週期,不然會致使觀察者被釋放帶來的Crashspa

監聽方法

觀察者須要實現observeValueForKeyPath:ofObject:change:context:方法,當KVO事件到來時會調用這個方法,若是沒有實現會致使Crashchange字典中存放KVO屬性相關的值,根據options時傳入的枚舉來返回。枚舉會對應相應key來從字典中取出值,例若有NSKeyValueChangeOldKey字段,存儲改變以前的舊值。

change中還有NSKeyValueChangeKindKey字段,和NSKeyValueChangeOldKey是平級的關係,來提供本次更改的信息,對應NSKeyValueChange枚舉類型的value。例如被觀察屬性發生改變時,字段爲NSKeyValueChangeSetting

若是被觀察對象是集合對象,在NSKeyValueChangeKindKey字段中會包含NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement的信息,表示集合對象的操做方式。

兼容的調用方式

調用KVO屬性對象時,不只能夠經過點語法和set語法進行調用,KVO兼容不少種調用方式。

// 直接調用set方法,或者經過屬性的點語法間接調用
[account setName:@"Savings"];
 
// 使用KVC的setValue:forKey:方法
[account setValue:@"Savings" forKey:@"name"];
 
// 使用KVC的setValue:forKeyPath:方法
[document setValue:@"Savings" forKeyPath:@"account.name"];

// 經過mutableArrayValueForKey:方法獲取到代理對象,並使用代理對象進行操做
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
複製代碼

實際應用

KVO主要用來作鍵值觀察操做,想要一個值發生改變後通知另外一個對象,則用KVO實現最爲合適。斯坦福大學的iOS教程中有一個很經典的案例,經過KVOModelController之間進行通訊。

斯坦福大學 KVO示例

注意點

KVOaddObserverremoveObserver須要是成對的,若是重複remove則會致使NSRangeException類型的Crash,若是忘記remove則會在觀察者釋放後再次接收到KVO回調時Crash

蘋果官方推薦的方式是,在init的時候進行addObserver,在deallocremoveObserver,這樣能夠保證addremove是成對出現的,是一種比較理想的使用方式。

手動調用KVO

KVO在屬性發生改變時的調用是自動的,若是想要手動控制這個調用時機,或想本身實現KVO屬性的調用,則能夠經過KVO提供的方法進行調用。

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}
複製代碼

能夠看到調用KVO主要依靠兩個方法,在屬性發生改變以前調用willChangeValueForKey:方法,在發生改變以後調用didChangeValueForKey:方法。

若是想控制當前對象的自動調用過程,也就是由上面兩個方法發起的KVO調用,則能夠重寫下面方法。方法返回YES則表示能夠調用,若是返回NO則表示不能夠調用。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
複製代碼

實現原理

KVO是經過isa-swizzling技術實現的(這句話是整個KVO實現的重點)。在運行時根據原類建立一箇中間類,這個中間類是原類的子類,並動態修改當前對象的isa指向中間類。而且將class方法重寫,返回原類的Class。因此蘋果建議在開發中不該該依賴isa指針,而是經過class實例方法來獲取對象類型。

測試代碼

爲了測試KVO的實現方式,咱們加入下面的測試代碼。首先建立一個KVOObject類,並在裏面加入兩個屬性,而後重寫description方法,並在內部打印一些關鍵參數。

@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 @"";
}
複製代碼

在另外一個類中分別建立兩個KVOObject對象,其中一個對象被觀察者經過KVO的方式監聽,另外一個對象則始終沒有被監聽。在KVO先後分別打印兩個對象的關鍵信息,看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
複製代碼

咱們發現對象被KVO後,其真正類型變爲了NSKVONotifying_KVOObject類,已經不是以前的類了。KVO會在運行時動態建立一個新類,將對象的isa指向新建立的類,新類是原類的子類,命名規則是NSKVONotifying_xxx的格式。KVO爲了使其更像以前的類,還會將對象的class實例方法重寫,使其更像原類。

在上面的代碼中還發現了_isKVOA方法,這個方法能夠當作使用了KVO的一個標記,系統可能也是這麼用的。若是咱們想判斷當前類是不是KVO動態生成的類,就能夠從方法列表中搜索這個方法。

重寫setter方法

KVO調用棧

KVO會重寫keyPath對應屬性的setter方法,沒有被KVO的屬性則不會重寫其setter方法。在重寫的setter方法中,修改值以前會調用willChangeValueForKey:方法,修改值以後會調用didChangeValueForKey:方法,這兩個方法最終都會被調用到observeValueForKeyPath:ofObject:change:context:方法中。

object_getClass

爲何上面調用runtimeobject_getClass函數,就能夠獲取到真正的類呢?

調用object_getClass函數後其返回的是一個Class類型,Classobjc_class定義的一個typedef別名,經過objc_class就能夠獲取到對象的isa指針指向的Class,也就是對象的類對象。

由此能夠推測,object_getClass函數內部返回的是對象的isa指針。

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
}
複製代碼

缺點

蘋果提供的KVO自身存在不少問題,首要問題在於,KVO若是使用不當很容易崩潰。例如重複addremove致使的CrashObserver被釋放致使的崩潰,keyPath傳錯致使的崩潰等。

在調用KVO時須要傳入一個keyPath,因爲keyPath是字符串的形式,因此其對應的屬性發生改變後,字符串沒有改變容易致使Crash。咱們能夠利用系統的反射機制將keyPath反射出來,這樣編譯器能夠在@selector()中進行合法性檢查。

NSStringFromSelector(@selector(isFinished))
複製代碼

KVO是一種事件綁定機制的實現,在keyPath對應的值發生改變後會回調對應的方法。這種數據綁定機制,在對象關係很複雜的狀況下,很容易致使很差排查的bug。例如keyPath對應的屬性被調用的關係很複雜,就不太建議對這個屬性進行KVO,能夠想一下RAC的信號腦補一下。

本身實現KVO

除了上面的缺點,KVO還不支持block語法,須要單獨重寫父類方法,這樣加上addremove方法就會致使代碼很分散。因此,我經過runtime簡單的實現了一個KVO,源碼放在個人Github上,叫作EasyKVO

self.object1 = [[KVOObject alloc] init];
[self.object1 lxz_addObserver:self originalSelector:@selector(name) callback:^(id observedObject, NSString *observedKey, id oldValue, id newValue) {
    // callback
}];

self.object1.name = @"lxz";
[self.object1 lxz_removeObserver:self originalSelector:@selector(name)];
複製代碼

調用代碼很簡單,直接經過lxz_addObserver:originalSelector:callback:方法就能夠添加KVO的監聽,能夠經過callbackblock接收屬性發生改變後的回調,並且方法的keyPath接收的是一個SEL類型參數,因此能夠經過@selector()傳入參數時進行方法合法性檢查,若是是未實現的方法直接就會報警告。

經過lxz_removeObserver:originalSelector:方法傳入觀察者和keyPath,當觀察者全部keyPath都移除後則從KVO中移除觀察者對象。

若是重複addObserverremoveObserver也沒事,內部有判斷邏輯。EasyKVO內部經過weak對觀察者作引用,並不會影響觀察者的生命週期,而且在觀察者釋放後不會致使Crash。一次add方法調用對應一個block,若是觀察者監聽多個keyPath屬性,不須要在block回調中判斷keyPath

注意

須要注意的是,EasyKVO只是作技術交流,不建議在項目中使用。由於KVO實現須要考慮不少狀況,繼承關係、多個觀察者等不少問題。

KVOController

想在項目中安全便捷的使用KVO的話,推薦Facebook的一個KVO開源第三方框架-KVOControllerKVOController本質上是對系統KVO的封裝,具備原生KVO全部的功能,並且規避了原生KVO的不少問題,兼容blockaction兩種回調方式。

源碼分析

從源碼來看仍是比較簡單的,主要分爲NSObjectCategoryFBKVOController兩部分。

FBKVOController

Category中提供了KVOControllerKVOControllerNonRetaining兩個屬性,顧名思義第一個會對observer產生強引用,第二個則不會。其內部代碼就是建立FBKVOController對象的代碼,並將建立出來的對象賦值給Category的屬性,直接經過這個Category就能夠懶加載建立FBKVOController對象。

- (FBKVOController *)KVOControllerNonRetaining
{
  id controller = objc_getAssociatedObject(self, NSObjectKVOControllerNonRetainingKey);
  
  if (nil == controller) {
    controller = [[FBKVOController alloc] initWithObserver:self retainObserved:NO];
    self.KVOControllerNonRetaining = controller;
  }
  
  return controller;
}
複製代碼

FBKVOController部分

FBKVOController中分爲三部分,_FBKVOInfo是一個私有類,這個類的功能很簡單,就是以結構化的形式保存FBKVOController所需的各個對象,相似於模型類的功能。

還有一個私有類_FBKVOSharedController,這是FBKVOController框架實現的關鍵。從命名上能夠看出其是一個單例,全部經過FBKVOController實現的KVO,觀察者都是它。每次經過FBKVOController添加一個KVO時,_FBKVOSharedController都會將本身設爲觀察者,並在其內部實現observeValueForKeyPath:ofObject:change:context:方法,將接收到的消息經過blockaction進行轉發。

其功能很簡單,經過observe:info:方法添加KVO監聽,並用一個NSHashTable保存_FBKVOInfo信息。經過unobserve:info:方法移除監聽,並從NSHashTable中將對應的_FBKVOInfo移除。這兩個方法內部都會調用系統的KVO方法。

在外界使用時須要用FBKVOController類,其內部實現了初始化以及添加和移除監聽的操做。在調用添加監聽方法後,其內部會建立一個_FBKVOInfo對象,並經過一個NSMapTable對象進行持有,而後會調用_FBKVOSharedController來進行註冊監聽。

使用FBKVOController的話,不須要手動調用removeObserver方法,在被監聽對象消失的時候,會在dealloc中調用remove方法。若是由於業務需求,能夠手動調用remove方法,重複調用remove方法不會有問題。

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];

    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
      return;
    }

    if (nil == infos) {
      infos = [NSMutableSet set];
      [_objectInfosMap setObject:infos forKey:object];
    }

    [infos addObject:info];

    [[_FBKVOSharedController sharedController] observe:object info:info];
}
複製代碼

由於FBKVOController的實現很簡單,因此這裏就很簡單的講講,具體實現能夠去Github下載源碼仔細分析一下。

相關文章
相關標籤/搜索