如何本身動手實現 KVO

        本文是 Objective-C Runtime 系列文章的第三篇。若是你對 Objective-C Runtime 還不是很瞭解,能夠先去看看前兩篇文章:html

  1. Objective-C Runtime
  2. Method Swizzling 和 AOP 實踐

        本篇會探究 KVO (Key-Value Observing) 實現機制,並去實踐一番 - 利用 Runtime 本身動手去實現 KVO 。git

KVO (Key-Value Observing)

KVO 是 Objective-C 對觀察者模式(Observer Pattern)的實現。也是 Cocoa Binding 的基礎。當被觀察對象的某個屬性發生更改時,觀察者對象會得到通知。github

有意思的是,你不須要給被觀察的對象添加任何額外代碼,就能使用 KVO 。這是怎麼作到的?app

KVO 實現機制

KVO 的實現也依賴於 Objective-C 強大的 Runtime 。Apple 的文檔有簡單提到過 KVO 的實現async

Automatic key-value observing is implemented using a technique called isa-swizzling... 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 ...ide

Apple 的文檔真是一筆帶過,惟一有用的信息也就是:被觀察對象的 isa 指針會指向一箇中間類,而不是原來真正的類。看來,Apple 並不但願過多暴露 KVO 的實現細節。不過,要是你用 runtime 提供的方法去深刻挖掘,全部被掩蓋的細節都會原形畢露。Mike Ash 早在 2009 年就作了這麼個探究函數

簡單概述下 KVO 的實現:優化

當你觀察一個對象時,一個新的類會動態被建立。這個類繼承自該對象的本來的類,並重寫了被觀察屬性的 setter 方法。天然,重寫的 setter 方法會負責在調用原 setter方法以前和以後,通知全部觀察對象值的更改。最後把這個對象的 isa 指針 ( isa 指針告訴 Runtime 系統這個對象的類是什麼 ) 指向這個新建立的子類,對象就神奇的變成了新建立的子類的實例。this

原來,這個中間類,繼承自本來的那個類。不只如此,Apple 還重寫了 -class 方法,企圖欺騙咱們這個類沒有變,就是本來那個類。更具體的信息,去跑一下 Mike Ash 的那篇文章裏的代碼就能明白,這裏就再也不重複。atom

KVO 缺陷

KVO 很強大,沒錯。知道它內部實現,或許能幫助更好地使用它,或在它出錯時更方便調試。但官方實現的 KVO 提供的 API 實在不怎麼樣。

好比,你只能經過重寫 -observeValueForKeyPath:ofObject:change:context: 方法來得到通知。想要提供自定義的 selector ,不行;想要傳一個 block ,門都沒有。並且你還要處理父類的狀況 - 父類一樣監聽同一個對象的同一個屬性。但有時候,你不知道父類是否是對這個消息有興趣。雖然 context 這個參數就是幹這個的,也能夠解決這個問題 - 在 -addObserver:forKeyPath:options:context: 傳進去一個父類不知道的 context。但總以爲框在這個 API 的設計下,代碼寫的很彆扭。至少至少,也應該支持 block 吧。

有很多人都以爲官方 KVO 很差使的。Mike Ash 的 Key-Value Observing Done Right,以及得到很多分享討論的 KVO Considered Harmful 都把 KVO 拿出來吊打了一番。因此在實際開發中 KVO 使用的情景並很少,更多時候仍是用 Delegate 或 NotificationCenter。

本身實現 KVO

若是沒找到理想的,就本身動手作一個。既然咱們對官方的 API 不太滿意,又知道如何去實現一個 KVO,那就嘗試本身動手寫一個簡易的 KVO 玩玩。

首先,咱們建立 NSObject 的 Category,並在頭文件中添加兩個 API:

typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block;

- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

接下來,實現 PG_addObserver:forKey:withBlock: 方法。邏輯並不複雜:

  1. 檢查對象的類有沒有相應的 setter 方法。若是沒有拋出異常;
  2. 檢查對象 isa 指向的類是否是一個 KVO 類。若是不是,新建一個繼承原來類的子類,並把 isa 指向這個新建的子類;
  3. 檢查對象的 KVO 類重寫過沒有這個 setter 方法。若是沒有,添加劇寫的 setter 方法;
  4. 添加這個觀察者
- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block
{
    // Step 1: Throw exception if its class or superclasses doesn't implement the setter
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        // throw invalid argument exception
    }

    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);

    // Step 2: Make KVO class if this is first time adding observer and 
    //          its class is not an KVO class yet
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }

    // Step 3: Add our kvo setter method if its class (not superclasses) 
    //          hasn't implemented the setter
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }

    // Step 4: Add this observation info to saved observation objects
    PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

再來一步一步細看。

第一步裏,先經過 setterForGetter() 方法得到相應的 setter 的名字(SEL)。也就是把 key 的首字母大寫,而後前面加上 set 後面加上 :,這樣 key 就變成了 setKey:。而後再用 class_getInstanceMethod 去得到 setKey: 的實現(Method)。若是沒有,天然要拋出異常。

第二步,咱們先看類名有沒有咱們定義的前綴。若是沒有,咱們就去建立新的子類,並經過 object_setClass() 修改 isa 指針。

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);

    if (clazz) {
        return clazz;
    }

    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);

    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);

    objc_registerClassPair(kvoClazz);

    return kvoClazz;
}

動態建立新的類須要用 objc/runtime.h 中定義的 objc_allocateClassPair() 函數。傳一個父類,類名,而後額外的空間(一般爲 0),它返回給你一個類。而後就給這個類添加方法,也能夠添加變量。這裏,咱們只重寫了 class 方法。哈哈,跟 Apple 同樣,這時候咱們也企圖隱藏這個子類的存在。最後 objc_registerClassPair() 告訴 Runtime 這個類的存在。

第三步,重寫 setter 方法。新的 setter 在調用原 setter 方法後,通知每一個觀察者(調用以前傳入的 block ):

static void kvo_setter(id self, SEL _cmd, id newValue)  
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);

    if (!getterName) {
        // throw invalid argument exception
    }

    id oldValue = [self valueForKey:getterName];

    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };

    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;

    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);

    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    for (PGObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

細心的同窗會發現咱們對 objc_msgSendSuper 進行類型轉換。在 Xcode 6 裏,新的 LLVM 會對 objc_msgSendSuper 以及 objc_msgSend 作嚴格的類型檢查,若是不作類型轉換。Xcode 會抱怨有 too many arguments 的錯誤。(在 WWDC 2014 的視頻 What new in LLVM 中有提到過這個問題。)

最後一步,把這個觀察的相關信息存在 associatedObject 裏。觀察的相關信息(觀察者,被觀察的 key, 和傳入的 block )封裝在 PGObservationInfo 類裏。

@interface PGObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PGObservingBlock block;

@end

就此,一個基本的 KVO 就能夠 work 了。固然,這只是一個一天多作出來的小東西,會有 bug,也有不少能夠優化完善的地方。但做爲 demo 演示如何利用 Runtime 動態建立類、如何實現 KVO,足已。

完整的例子能夠從這裏下載:ImplementKVO

相關文章
相關標籤/搜索