分析實現-實現KVO

原文鏈接

基于觀察者設計模式,蘋果實現了notificationkvo兩套監聽機制,兩者都實現了一對多的監聽支持。通知在設計上暴露了notificationCenter這個中心類,通過公開的接口和數據類型,不難猜測出其實現方式。但KVO僅在NSObject中暴露了幾個接口,同時缺乏必要的中間類,文檔中也只有模糊的介紹,這讓人不由地對其實現機制產生興趣。

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 ..

翻譯過來就是:KVO是通過一種稱作isa-swizzling的機制實現的,這個機制會在被觀察對象的屬性被監聽時修改對象的isa指針,讓指針指向一個中間類而非對象自身的類。

isa

通過文檔的描述,可以得出isa指針是KVO的實現機制中最為核心的變量,那么什么是isa指針?如果你能使用英語而非拼音來書寫代碼,那么一定能夠明白Objective-C翻譯過來就是C語言的面向對象。換句話說:

OC的所有對象都是封裝于C語言的結構體

雖然可以想象到,使用struct來實現面向對象的特性必然是一個十分復雜的過程,但繼承的實現我們可以輕易的想象出來:在自身結構內部預留父結構體的變量。打個比方,NSObject的結構體為objc_object,存儲了一個isa指針,假如存在子類Person,翻閱objc-private可以確定子類的結構組成:

typedef struct objc_object *id;
typedef struct objc_class *Class;
struct objc_object {
    Class isa;
};

struct objc_class : objc_object {
    // Class isa;
    Class superClass;
    cache_t cache;
    class_data_bits_t bits;
    ......
}

由于id類型屬于通配類型,可以用來指向所有OC中的對象,根據其實現結構來看,可以說每一個OC對象都存在一個isa指針用來表示對象類型信息:

isa-swizzling

函數object_setClass提供了修改isa指針的手段,前面已經提到了isa用來表示對象的所屬類型,那么交換isa指針可以看做是修改對象的所屬類型:

/// NSObject.mm
- (Class)class {
    return object_getClass(self);
}

/// code
id obj = [NSObject new];
NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));

object_setClass(obj, [NSString class]);
NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));

/// log
2018-01-25 09:58:46.870577+0800 Test[11398:955919] -class: NSObject, object_getClass: NSObject
2018-01-25 09:58:46.870743+0800 Test[11398:955919] -class: NSString, object_getClass: NSString

方法(+/-)(Class)class的實現中采用object_getclass函數獲取對象的所屬類型,由于class方法存在被重寫來誤導使用者的可能性,可以直接調用object_getclass來獲取正確的對象類型,通過這個函數可以窺見KVO的實現:

- (void)test {
    id obj = [TestObj new];
    NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));
    [obj addObserver: [NSObject new] forKeyPath: @"val" options: NSKeyValueObservingOptionNew context: nil];
    NSLog(@"-class: %@, object_getClass: %@", NSStringFromClass([obj class]), NSStringFromClass(object_getClass(obj)));
    
    Class realClass = object_getClass(obj);
    NSLog(@"%@", NSStringFromClass(class_getSuperclass(realClass)));
}

// log
2018-01-25 10:03:24.832764+0800 Test[11398:955919] -class: TestObj, object_getClass: TestObj
2018-01-25 10:03:24.833267+0800 Test[11398:955919] -class: TestObj, object_getClass: NSKVONotifying_TestObj
2018-01-25 10:03:24.833283+0800 Test[11398:955919] realClass's super class is: TestObj

mock in iOS中我曾經提到過要完全模擬一個對象包括兩種手段:inherit或者isa_swizzling,結合蘋果官方文檔的說明,很明顯蘋果采用了后者。

type-encode

KVO的實現基礎之一是被監控對象必須擁有相應的setter方法,換句話說只有ivar的類是無法進行監控的:

@interface UnableObservedClasss : NSobject
{
@public
    id _val1;
    id _val2;
}

@end

在監控過程中,KVO生成的新子類需要重寫setter的實現,在屬性發生修改的上下文插入執行回調的代碼:

- (void)setVal: (id)val {
    [self willChangeValueForKey: @"val"];
    [super setVal: val];
    [self didChangeValueForKey: @"val"];
}

要實現一套通用的KVO機制時,是不能預設什么類型的property會被監控,因此如果無法區分監控屬性的類型,是無法動態的去生成setter,我們需要使用到type encoding機制來協助完成這一工作。OC使用特定的字符編碼表示某一種具體的數據類型,使用@encode([obj class])可以獲取變量類型所對應的字符編碼。下面列出官方文檔中的編碼對應表:

編碼 類型
c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B bool _Bool
v void
* char*
@ id
# Class
: SEL
[array type] array
{name=type...} struct
(name=type...) union
bnum a bit field of num bits
^type pointer to type
? unknown

對于單個property來說,通過property_copyAttributeList函數可以獲取property的修飾符信息和類型信息,所有信息采用結構體進行映射表示:

typedef struct {
    const char * _Nonnull name;     /// 修飾編碼
    const char * _Nonnull value;    /// 具體內容
} objc_property_attribute_t;

有兩個重要的修飾編碼:T表示類型編碼,通過匹配編碼表確認類型;S表示屬性含有setter,可以動態的生成KVO的方法

實現

參照YYModel對于屬性setter的封裝實現:

/// 獲取監控的屬性
objc_property_t getKVOProperty(Class cls, NSString *keyPath) {
    if (!keyPath || !cls) {
        return NULL;
    }
    
    objc_property_t res = NULL;
    unsigned int count = 0;
    const char *property_name = keyPath.UTF8String;
    objc_property_t *properties = class_copyPropertyList(cls, &count);
    
    for (unsigned int idx = 0; idx < count; idx++) {
        objc_property_t property = properties[idx];
        if (strcmp(property_name, property_getName(property)) == 0) {
            res = property;
            break;
        }
    }
    free(properties);
    return res;
}

/// 檢測屬性是否存在setter方法
BOOL ifPropertyHasSetter(objc_property_t property) {
    BOOL res = NO;
    unsigned int attrCount;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'S') {
            res = YES;
        }
    }
    free(attrs);
    return res;
}

/// 獲取屬性的數據類型
YYEncodingType getPropertyType(objc_property_t) {
    unsigned int attrCount;
    YYEncodingType type = YYEncodingTypeUnknown;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    
    for (unsigned int idx = 0; idx < attrCount; idx++) {
        if (attrs[idx].name[0] == 'T') {
            type = YYEncodingGetType(attrs[idx].value);
        }
    }
    free(attrs);
    return type;
}

/// 根據setter名稱獲取屬性名
NSString *getPropertyNameFromSelector(SEL selector) {
    NSString *selName = [NSStringFromSelector(selector) substringFromIndex: 3];
    NSString *firstAlpha = [[selName substringToIndex: 1] lowercaseString];
    return [selName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstAlpha];
}

/// 根據屬性名獲取setter名稱
SEL getSetterFromKeyPath(NSString *keyPath) {
    NSString *firstAlpha = [[keyPath substringToIndex: 1] uppercaseString];
    NSString *selName = [NSString stringWithFormat: @"set%@", [keyPath stringByReplacingCharactersInRange: NSMakeRange(0,  1) withString: firstAlpha]];
    return NSSelectorFromString(selName);
}

/// 設置bool屬性的kvo setter
static void setBoolVal(id self, SEL _cmd, BOOL val) {
    NSString *name = getPropertyNameFromSelector(_cmd);
    void (*objc_msgSendKVO)(void *, SEL, NSString *) = (void *)objc_msgSend;
    void (*objc_msgSendSuperKVO)(void *, SEL, BOOL) = (void *)objc_msgSendSuper;
    
    objc_msgSendKVO(self, @selector(willChangeValueForKey:), val);
    objc_msgSendSuperKVO(self, _cmd, val);
    objc_msgSendKVO(self, @selector(didChangeValueForKey:), val);
}

/// KVO實現
static void addObserver(id observedObj, id observer, NSString *keyPath) {
    objc_property_t observedProperty = getKVOProperty([observedObj class], keyPath);
    if (!ifPropertyHasSetter(observedProperty)) {
        return;
    }
    
    NSString *kvoClassName = [@"SLObserved_" stringByAppendString: NSStringFromClass([observedObj class])];
    Class kvoClass = NSClassFromString(kvoClassName);
    if (!kvoClass)) {
        kvoClass = objc_allocateClassPair([observedObj class], kvoClassName.UTF8String, NULL);
        
        Class(^classBlock)(id) = ^Class(id self) {
            return class_getSuperclass([self class]);
        };
        class_addMethod(kvoClass, @selector(class), imp_implementationWithBlock(classBlock), method_getTypeEncoding(class_getMethodImplementation([observedObj class], @selector(class))));
        objc_registerClassPair(kvoClass);
    }
    
    YYEncodingType type = getPropertyType(observedProperty);
    SEL setter = getSetterFromKeyPath(observedProperty);
    switch (type) {
        case YYEncodingTypeBool: {
            class_addMethod(kvoClass, setter, (IMP)setBoolVal, method_getTypeEncoding(class_getMethodImplementation([observedObj class], setter)));
        }   break;
        ......
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,788評論 0 9
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,046評論 0 26
  • 序言在iOS開發中,蘋果提供了許多機制給我們進行回調。KVO(key-value-observing)是一種十分有...
    陌尚煙雨遙閱讀 504評論 0 0
  • 在iOS開發中,蘋果提供了許多機制給我們進行回調。KVO(key-value-observing)是一種十分有趣的...
    流沙3333閱讀 369評論 0 0
  • iOS--KVO的實現原理與具體應用 長時間不用容易忘,這篇文章挺好的.轉載自看本文分為2個部分:概念與應用。概念...
    超_iOS閱讀 1,452評論 0 17