基于觀察者設計模式,蘋果實現了notification
和kvo
兩套監聽機制,兩者都實現了一對多
的監聽支持。通知在設計上暴露了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;
......
}
}