關(guān)于KVC/KVO的實(shí)現(xiàn)原理,網(wǎng)上的相關(guān)介紹文章很多,但大部分說的比較抽象,難以真切的理解,下面我們直接擼代碼來實(shí)地探討下。
演示代碼地址:https://github.com/Assuner-Lee/KVC-KVO-Test.git
KVC 演示代碼
ASClassA.h
#import <Foundation/Foundation.h>
@interface ASModel : NSObject
@property (nonatomic, strong) NSString *_modelString;
@end
@interface ASClassA : NSObject
@property (nonatomic, strong) NSString *stringA;
@property (nonatomic, strong) ASModel *modelA;
@end
ASClassA.m
#import "ASClassA.h"
@implementation ASModel
- (void)set_modelString:(NSString *)_modelString {
__modelString = _modelString;
NSLog(@"執(zhí)行 setter _modelString");
}
- (void)setModelString:(NSString *)modelString {
NSLog(@"執(zhí)行 setter modelString");
}
- (void)setNoExist1:(NSString *)noExist {
NSLog(@"執(zhí)行 setter noExist1 ");
}
@end
@implementation ASClassA
- (void)setStringA:(NSString *)stringA {
NSLog(@"執(zhí)行 setter stringA");
_stringA = stringA;
- (instancetype)init {
if (self = [super init]) {
self.modelA = [[ASModel alloc] init];
}
return self;
}
@end
main.m
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "ASClassA.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
ASClassA *objectA = [[ASClassA alloc] init];
objectA.stringA = @"stringA setter"; // setter
① [objectA setValue:@"stringA KVC" forKey:@"stringA"]; // kvc
② [objectA setValue:@"_stringA KVC" forKey:@"_stringA"]; // kvc _
NSLog(@" objectA.stringA 值: %@", objectA.stringA);
NSLog(@"---------------------------------------------------------");
③ [objectA setValue:@"_modelString kvc" forKeyPath:@"modelA._modelString"]; //setter
④ [objectA setValue:@"modelString kvc" forKeyPath:@"modelA.modelString"]; // kvc 不存在的屬性
⑤ [objectA setValue:@"__modelString kvc" forKeyPath:@"modelA.__modelString"]; //kvc _
⑥ [objectA setValue:@"noExist1" forKeyPath:@"modelA.noExist1"]; //kvc 不存在的屬性
NSLog(@"objectA.modelA._modelString 值: %@", objectA.modelA._modelString);
NSLog(@"---------------------------------------------------------");
⑦ NSString *s1 = [objectA valueForKeyPath:@"modelA._modelString"];
⑧ NSString *s2 = [objectA valueForKeyPath:@"modelA.modelString"];
⑨? NSString *s3 = [objectA valueForKeyPath:@"modelA.__modelString"];
}
return 0;
}
運(yùn)行結(jié)果
①->⑨全部執(zhí)行成功; 其中①③④⑥ 執(zhí)行了setter方法,⑦⑧執(zhí)行了getter方法,②⑤⑨直接訪問的實(shí)例變量。
原因
其實(shí)點(diǎn)進(jìn)去valueForKey: 或setValueForKey: 幫助文檔已經(jīng)講得很清楚了
valueForKey:
The default implementation of this method does the following:
1. Searches the class of the receiver for an accessor method whose name matches the pattern -get<Key>, -<key>, or -is<Key>, in that order. If such a method is found it is invoked. If the type of the method's result is an object pointer type the result is simply returned. If the type of the result is one of the scalar types supported by NSNumber conversion is done and an NSNumber is returned. Otherwise, conversion is done and an NSValue is returned (new in Mac OS 10.5: results of arbitrary type are converted to NSValues, not just NSPoint, NRange, NSRect, and NSSize).
2 (introduced in Mac OS 10.7). Otherwise (no simple accessor method is found), searches the class of the receiver for methods whose names match the patterns -countOf<Key> and -indexIn<Key>OfObject: and -objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSOrderedSet class) and also -<key>AtIndexes: (corresponding to -[NSOrderedSet objectsAtIndexes:]). If a count method and an indexOf method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSOrderedSet methods is returned. Each NSOrderedSet message sent to the collection proxy object will result in some combination of -countOf<Key>, -indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, and -<key>AtIndexes: messages being sent to the original receiver of -valueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -get<Key>:range: that method will be used when appropriate for best performance.
3. Otherwise (no simple accessor method or set of ordered set access methods is found), searches the class of the receiver for methods whose names match the patterns -countOf<Key> and -objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and (introduced in Mac OS 10.4) also -<key>AtIndexes: (corresponding to -[NSArray objectsAtIndexes:]). If a count method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSArray methods is returned. Each NSArray message sent to the collection proxy object will result in some combination of -countOf<Key>, -objectIn<Key>AtIndex:, and -<key>AtIndexes: messages being sent to the original receiver of -valueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -get<Key>:range: that method will be used when appropriate for best performance.
4 (introduced in Mac OS 10.4). Otherwise (no simple accessor method or set of ordered set or array access methods is found), searches the class of the receiver for a threesome of methods whose names match the patterns -countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class). If all three such methods are found a collection proxy object that responds to all NSSet methods is returned. Each NSSet message sent to the collection proxy object will result in some combination of -countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>: messages being sent to the original receiver of -valueForKey:.
5. Otherwise (no simple accessor method or set of collection access methods is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found, the value of the instance variable in the receiver is returned, with the same sort of conversion to NSNumber or NSValue as in step 1.
6. Otherwise (no simple accessor method, set of collection access methods, or instance variable is found), invokes -valueForUndefinedKey: and returns the result. The default implementation of -valueForUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.
簡(jiǎn)而言之:
1.訪問器匹配:先尋找與getKey,key,isKey, (實(shí)測(cè)還有_key)同名的方法,返回值為對(duì)象類型。
2.實(shí)例變量匹配:尋找與_key,_isKey , key,isKey,同名的實(shí)例變量
setValueForKey:
The default implementation of this method does the following:
1. Searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found the type of its parameter is checked. If the parameter type is not an object pointer type but the value is nil -setNilValueForKey: is invoked. The default implementation of -setNilValueForKey: raises an NSInvalidArgumentException, but you can override it in your application. Otherwise, if the type of the method's parameter is an object pointer type the method is simply invoked with the value as the argument. If the type of the method's parameter is some other type the inverse of the NSNumber/NSValue conversion done by -valueForKey: is performed before the method is invoked.
2. Otherwise (no accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found and its type is an object pointer type the value is retained and the result is set in the instance variable, after the instance variable's old value is first released. If the instance variable's type is some other type its value is set after the same sort of conversion from NSNumber or NSValue as in step 1.
3. Otherwise (no accessor method or instance variable is found), invokes -setValue:forUndefinedKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.
簡(jiǎn)而言之:
1.存取器匹配:先尋找與setKey同名的方法,且參數(shù)要為一個(gè)對(duì)象類型
2.實(shí)例變量匹配:尋找與_key,_isKey,key,isKey同名的實(shí)例變量,直接賦值。
其他
當(dāng)我們使用id objectA = ObjectB.value2; 時(shí)是否代表objectB有一個(gè)value2的屬性呢,實(shí)際上不一定, "."操作只是去尋找一個(gè)名稱匹配參數(shù)匹配的方法, 我們習(xí)以為常的引用屬性只是因?yàn)閷傩詣偤糜術(shù)etter,setter符合要求而已,屬性的實(shí)質(zhì)為一個(gè)實(shí)例變量加存取方法(有些實(shí)例變量沒有存取方法,而有些存取方法并沒有對(duì)應(yīng)的實(shí)例變量)... 例如object.class,NSObject中 并沒有class屬性,只有一個(gè)class方法。 KVC是一種高效的取設(shè)值的方法,而無論這個(gè)鍵是否暴露出來。
KVO演示代碼
ASClassA.h
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface ASClassA : NSObject
@property (nonatomic, assign) NSUInteger value;
@property (nonatomic, assign) IMP imp;
@property (nonatomic, assign) IMP classImp;
@end
ASClassA.m
#import "ASClassA.h"
@implementation ASClassA
- (void)setValue:(NSUInteger)value {
_value = value;
}
- (IMP)imp {
return [self methodForSelector:@selector(setValue:)];
}
- (IMP)classImp {
return [self methodForSelector:@selector(class)];
}
@end
ASClassB.h
#import <Foundation/Foundation.h>
@interface ASClassB : NSObject
- (NSString *)classssss;
- (void)setClassssss;
@end
ASClassB.m
#import "ASClassB.h"
@implementation ASClassB
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
NSLog(@"B接收到變化");
}
- (NSString *)classssss {
return @"classssss";
}
- (void)setClassssss {
}
@end
ASClassC.h
#import <Foundation/Foundation.h>
@interface ASClassC : NSObject
@end
ASClassC.m
#import "ASClassC.h"
@implementation ASClassC
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
NSLog(@"C接收到變化");
}
@end
main.m
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
#import <Foundation/Foundation.h>
#import "ASClassA.h"
#import "ASClassB.h"
#import "ASClassC.h"
NSArray<NSString *> *getProperties(Class aClass) {
unsigned int count;
objc_property_t *properties = class_copyPropertyList(aClass, &count);
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
const char *cName = property_getName(property);
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
[mArray addObject:name];
}
return mArray.copy;
}
NSArray<NSString *> *getIvars(Class aClass) {
unsigned int count;
Ivar *ivars = class_copyIvarList(aClass, &count);
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *cName = ivar_getName(ivar);
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
[mArray addObject:name];
}
return mArray.copy;
}
NSArray<NSString *> *getMethods(Class aClass) {
unsigned int count;
Method *methods = class_copyMethodList(aClass, &count);
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
NSString *selectorName = NSStringFromSelector(selector);
[mArray addObject:selectorName];
}
return mArray.copy;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
ASClassA *objectA = [[ASClassA alloc] init];
ASClassB *objectB = [[ASClassB alloc] init];
ASClassC *objectC = [[ASClassC alloc] init];
NSString *bbb = objectB.classssss;
//objectB.classssss = @"";
Class classA1 = object_getClass(objectA);
Class classA1C = [objectA class]; // objectA.class;
NSLog(@"before objectA: %@", classA1);
NSArray *propertiesA1 = getProperties(classA1);
NSArray *ivarsA1 = getIvars(classA1);
NSArray *methodsA1 = getMethods(classA1);
IMP setterA1IMP = objectA.imp;
IMP classA1IMP = objectA.classImp;
Class classB1 = object_getClass(objectB);
NSLog(@"before objectA: %@", classB1);
NSArray *propertiesB1 = getProperties(classB1);
NSArray *ivarsB1 = getIvars(classB1);
NSArray *methodsB1 = getMethods(classB1);
[objectA addObserver:objectB forKeyPath:@"value" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
[objectA addObserver:objectC forKeyPath:@"value" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
Class classA2 = object_getClass(objectA);
Class classA2C = [objectA class];
BOOL isSame = [objectA isEqual:[objectA self]];
id xxxx = [[classA2 alloc] init];
NSLog(@"after objectA: %@", classA2);
NSArray *propertiesA2 = getProperties(classA2);
NSArray *ivarsA2 = getIvars(classA2);
NSArray *methodsA2 = getMethods(classA2);
IMP setterA2IMP = objectA.imp;
IMP classA2IMP = objectA.classImp;
Class classB2 = object_getClass(objectB);
NSLog(@"before objectA: %@", classB2);
NSArray *propertiesB2 = getProperties(classB2);
NSArray *ivarsB2 = getIvars(classB2);
NSArray *methodsB2 = getMethods(classB2);
NSObject *object = [[NSObject alloc] init];
NSArray *propertiesObj = getProperties([object class]);
NSArray *methodsObj = getMethods([object class]);
NSArray *ivarsObj = getIvars([object class]);
BOOL isSameClass = [classA1 isEqual:classA2];
BOOL isSubClass = [classA2 isSubclassOfClass:classA1];
objectA.value = 10;
[objectA removeObserver:objectB forKeyPath:@"value"];
[objectA removeObserver:objectC forKeyPath:@"value"];
NSNumber *integerNumber = [NSNumber numberWithInteger:1];
Class integerNumberClass = object_getClass(integerNumber);
NSNumber *boolNumber = [NSNumber numberWithBool:YES];
Class boolNumberClass = object_getClass(boolNumber);
}
return 0;
}
#pragma clang diagnostic pop
運(yùn)行結(jié)果



分析以上結(jié)果
我們通過抓取objectA在被objectB, objectC觀察前和觀察后的 類的類型,屬性列表,變量列表,方法列表,得出:
① class: 被觀察前,objectA為ASClassA類型, 被觀察后,變?yōu)榱?code>NSKVONotifying_ASClassA類型,且這個(gè)類為ASClassA的子類(通過isa指向改變,事實(shí)上,object_getClass(objectA) 和 objectA->isa方法等價(jià))。
② 屬性,實(shí)例變量:無變化。
③ 方法列表:NSKVONotifying_ASClassA 出現(xiàn)了四個(gè)新的方法,
我們可以注意到,被觀察的值setValue:方法的實(shí)現(xiàn)由
([ASClassA setValue:] at ASClassA.m)變?yōu)榱?code>(Foundation_NSSetUnsignedLongLongValueAndNotify)。這個(gè)被重寫的setter方法在原有的實(shí)現(xiàn)前后插入了[self willChangeValueForKey:@“name”]; 調(diào)用存取方法之前總調(diào)[super setValue:newName forKey:@”name”]; [self didChangeValueForKey:@”name”]; 等,以觸發(fā)觀察者的響應(yīng)。
然后class方法由(libobjc.A.dylib -[NSObject class])
變?yōu)榱?code>(Foundation_NSKVOClass),
#######這也解釋了我們?cè)诒挥^察前 被觀察后執(zhí)行[objectA class]方法得到結(jié)果不同的原因,-(Class)class方法的實(shí)現(xiàn)本來就是object_getClass,但在被觀察后class方法和object_getClass結(jié)果卻不一樣,事實(shí)是class方法被重寫了,class方法總能得到ASClassA
dealloc方法: 觀察移除后使class變回去ASClassA(通過isa指向),
_isKVO: 判斷被觀察者自己是否同時(shí)也觀察了其他對(duì)象
事實(shí)上

簡(jiǎn)而言之,蘋果使用了一種isa交換的技術(shù),當(dāng)
objectA被觀察后,objectA對(duì)象的isa指針被指向了一個(gè)新建的ASClassA的子類NSKVONotifying_ASClassA,且這個(gè)子類重寫了被觀察值的setter方法和class方法,dealloc和_isKVO方法,然后使objectA對(duì)象的isa指針指向這個(gè)新建的類,然后事實(shí)上objectA變?yōu)榱薔SKVONotifying_ASClassA的實(shí)例對(duì)象,執(zhí)行方法要從這個(gè)類的方法列表里找。(同時(shí)蘋果警告我們,通過isa獲取類的類型是不可靠的,通過class方法總是能得到正確的類=_=!!).
更多關(guān)于OC對(duì)象,類,isa, 屬性, 變量, 方法的介紹
請(qǐng)參考我的另一篇博文
Runtime簡(jiǎn)介
思考
由于研究方法有限,并不能知道被觀察者的值改變后,以何種方式去通知觀察者,并使其執(zhí)行實(shí)現(xiàn)的對(duì)應(yīng)的方法的,我們可以猜想,也許是蘋果慣用的,像維護(hù)對(duì)象們的引用計(jì)數(shù),和weak修飾的對(duì)象的存亡 一樣,建立了一張hash表去對(duì)應(yīng)觀察者,被觀察者的地址或其他。能力有限,尚不能得知。
謝謝觀看
歡迎大家指出文中的錯(cuò)誤!
演示代碼地址:https://github.com/Assuner-Lee/KVC-KVO-Test.git
下一篇:最簡(jiǎn)單的仿微信圖片選擇器 LPDQuoteImagesView