基于觀察者設(shè)計(jì)模式,蘋果實(shí)現(xiàn)了notification
和kvo
兩套監(jiān)聽(tīng)機(jī)制,兩者都實(shí)現(xiàn)了一對(duì)多
的監(jiān)聽(tīng)支持。通知在設(shè)計(jì)上暴露了notificationCenter
這個(gè)中心類,通過(guò)公開(kāi)的接口和數(shù)據(jù)類型,不難猜測(cè)出其實(shí)現(xiàn)方式。但KVO
僅在NSObject
中暴露了幾個(gè)接口,同時(shí)缺乏必要的中間類,文檔中也只有模糊的介紹,這讓人不由地對(duì)其實(shí)現(xiàn)機(jī)制產(chǎn)生興趣。
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 ..
翻譯過(guò)來(lái)就是:KVO
是通過(guò)一種稱作isa-swizzling
的機(jī)制實(shí)現(xiàn)的,這個(gè)機(jī)制會(huì)在被觀察對(duì)象的屬性被監(jiān)聽(tīng)時(shí)修改對(duì)象的isa
指針,讓指針指向一個(gè)中間類而非對(duì)象自身的類。
isa
通過(guò)文檔的描述,可以得出isa
指針是KVO
的實(shí)現(xiàn)機(jī)制中最為核心的變量,那么什么是isa
指針?如果你能使用英語(yǔ)而非拼音來(lái)書寫代碼,那么一定能夠明白Objective-C
翻譯過(guò)來(lái)就是C
語(yǔ)言的面向?qū)ο?。換句話說(shuō):
OC的所有對(duì)象都是封裝于C語(yǔ)言的結(jié)構(gòu)體
雖然可以想象到,使用struct
來(lái)實(shí)現(xiàn)面向?qū)ο蟮奶匦员厝皇且粋€(gè)十分復(fù)雜的過(guò)程,但繼承的實(shí)現(xiàn)我們可以輕易的想象出來(lái):在自身結(jié)構(gòu)內(nèi)部預(yù)留父結(jié)構(gòu)體的變量。打個(gè)比方,NSObject
的結(jié)構(gòu)體為objc_object
,存儲(chǔ)了一個(gè)isa
指針,假如存在子類Person
,翻閱objc-private可以確定子類的結(jié)構(gòu)組成:
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
類型屬于通配類型,可以用來(lái)指向所有OC
中的對(duì)象,根據(jù)其實(shí)現(xiàn)結(jié)構(gòu)來(lái)看,可以說(shuō)每一個(gè)OC
對(duì)象都存在一個(gè)isa
指針用來(lái)表示對(duì)象類型信息:
isa-swizzling
函數(shù)object_setClass
提供了修改isa
指針的手段,前面已經(jīng)提到了isa
用來(lái)表示對(duì)象的所屬類型,那么交換isa
指針可以看做是修改對(duì)象的所屬類型:
/// 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
的實(shí)現(xiàn)中采用object_getclass
函數(shù)獲取對(duì)象的所屬類型,由于class
方法存在被重寫來(lái)誤導(dǎo)使用者的可能性,可以直接調(diào)用object_getclass
來(lái)獲取正確的對(duì)象類型,通過(guò)這個(gè)函數(shù)可以窺見(jiàn)KVO
的實(shí)現(xiàn):
- (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中我曾經(jīng)提到過(guò)要完全模擬一個(gè)對(duì)象包括兩種手段:inherit
或者isa_swizzling
,結(jié)合蘋果官方文檔的說(shuō)明,很明顯蘋果采用了后者。
type-encode
KVO
的實(shí)現(xiàn)基礎(chǔ)之一是被監(jiān)控對(duì)象必須擁有相應(yīng)的setter
方法,換句話說(shuō)只有ivar
的類是無(wú)法進(jìn)行監(jiān)控的:
@interface UnableObservedClasss : NSobject
{
@public
id _val1;
id _val2;
}
@end
在監(jiān)控過(guò)程中,KVO
生成的新子類需要重寫setter
的實(shí)現(xiàn),在屬性發(fā)生修改的上下文插入執(zhí)行回調(diào)的代碼:
- (void)setVal: (id)val {
[self willChangeValueForKey: @"val"];
[super setVal: val];
[self didChangeValueForKey: @"val"];
}
要實(shí)現(xiàn)一套通用的KVO
機(jī)制時(shí),是不能預(yù)設(shè)什么類型的property
會(huì)被監(jiān)控,因此如果無(wú)法區(qū)分監(jiān)控屬性的類型,是無(wú)法動(dòng)態(tài)的去生成setter
,我們需要使用到type encoding
機(jī)制來(lái)協(xié)助完成這一工作。OC
使用特定的字符編碼表示某一種具體的數(shù)據(jù)類型,使用@encode([obj class])
可以獲取變量類型所對(duì)應(yīng)的字符編碼。下面列出官方文檔中的編碼對(duì)應(yīng)表:
編碼 | 類型 |
---|---|
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 |
對(duì)于單個(gè)property
來(lái)說(shuō),通過(guò)property_copyAttributeList
函數(shù)可以獲取property
的修飾符信息和類型信息,所有信息采用結(jié)構(gòu)體進(jìn)行映射表示:
typedef struct {
const char * _Nonnull name; /// 修飾編碼
const char * _Nonnull value; /// 具體內(nèi)容
} objc_property_attribute_t;
有兩個(gè)重要的修飾編碼:T
表示類型編碼,通過(guò)匹配編碼表確認(rèn)類型;S
表示屬性含有setter
,可以動(dòng)態(tài)的生成KVO
的方法
實(shí)現(xiàn)
參照YYModel
對(duì)于屬性setter
的封裝實(shí)現(xiàn):
/// 獲取監(jiān)控的屬性
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;
}
/// 檢測(cè)屬性是否存在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;
}
/// 獲取屬性的數(shù)據(jù)類型
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;
}
/// 根據(jù)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];
}
/// 根據(jù)屬性名獲取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);
}
/// 設(shè)置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實(shí)現(xiàn)
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;
......
}
}