用程式碼探討 KVC/KVO 的實現原理

餓了麼物流技術團隊發表於2018-04-05
2017-03-11 | Assuner | iOS

概述

關於KVC/KVO的實現原理,網上的相關介紹文章很多,但大部分說的比較抽象,難以真切的理解,下面我們直接擼程式碼來實地探討下。

演示程式碼地址: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(@"執行 setter _modelString");
}

- (void)setModelString:(NSString *)modelString {
    NSLog(@"執行 setter modelString");
}

- (void)setNoExist1:(NSString *)noExist {
    NSLog(@"執行 setter noExist1 ");
}

@end


@implementation ASClassA

- (void)setStringA:(NSString *)stringA {
     NSLog(@"執行 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;
}

複製程式碼

執行結果

①->⑨全部執行成功; 其中①③④⑥ 執行了setter方法,⑦⑧執行了getter方法,②⑤⑨直接訪問的例項變數。

原因

其實點進去valueForKey: 或setValueForKey: 幫助文件已經講得很清楚了

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.
複製程式碼

簡而言之:

1.訪問器匹配:先尋找與key,isKey, getKey (實測還有_key)同名的方法,返回值為物件型別。

2.例項變數匹配:尋找與key, _key,isKey,_isKey同名的例項變數

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.
複製程式碼

簡而言之:

1.存取器匹配:先尋找與setKey同名的方法,且引數要為一個物件型別

2.例項變數匹配:尋找與key,_isKey,_key,isKey同名的例項變數,直接賦值。

其他

當我們使用id objectA = ObjectB.value2; 時是否代表objectB有一個value2的屬性呢,實際上不一定, "."操作只是去尋找一個名稱匹配引數匹配的方法, 我們習以為常的引用屬性只是因為屬性剛好有getter,setter符合要求而已,屬性的實質為一個例項變數加存取方法(有些例項變數沒有存取方法,而有些存取方法並沒有對應的例項變數)... 例如object.classNSObject中 並沒有class屬性,只有一個class方法。 KVC是一種高效的取設值的方法,而無論這個鍵是否暴露出來。


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

複製程式碼

執行結果

用程式碼探討 KVC/KVO 的實現原理

用程式碼探討 KVC/KVO 的實現原理

執行結果

分析以上結果

我們通過抓取objectA在被objectB, objectC觀察前和觀察後的 類的型別,屬性列表,變數列表,方法列表,得出:

① class: 被觀察前,objectAASClassA型別, 被觀察後,變為了NSKVONotifying_ASClassA型別,且這個類為ASClassA的子類(通過isa指向改變,事實上,object_getClass(objectA)objectA->isa方法等價)。

② 屬性,例項變數:無變化。

③ 方法列表:NSKVONotifying_ASClassA 出現了四個新的方法,

我們可以注意到,被觀察的值setValue:方法的實現由 ([ASClassA setValue:] at ASClassA.m)變為了(Foundation_NSSetUnsignedLongLongValueAndNotify)。這個被重寫的setter方法在原有的實現前後插入了[self willChangeValueForKey:@“name”]; 呼叫存取方法之前總調[super setValue:newName forKey:@”name”]; [self didChangeValueForKey:@”name”]; 等,以觸發觀察者的響應。 然後class方法由(libobjc.A.dylib -[NSObject class]) 變為了(Foundation_NSKVOClass),

這也解釋了我們在被觀察前被觀察後執行[objectA class]方法得到結果不同的原因,-(Class)class方法的實現本來就是object_getClass,但在被觀察後class方法和object_getClass結果卻不一樣,事實是class方法被重寫了,class方法總能得到ASClassA

dealloc方法: 觀察移除後使class變回去ASClassA(通過isa指向), _isKVO: 判斷被觀察者自己是否同時也觀察了其他物件

事實上

蘋果開發者文件

蘋果開發者文件

簡而言之,蘋果使用了一種isa交換的技術,當objectA被觀察後,objectA物件的isa指標被指向了一個新建的ASClassA的子類NSKVONotifying_ASClassA,且這個子類重寫了被觀察值的setter方法和class方法,dealloc_isKVO方法,然後使objectA物件的isa指標指向這個新建的類,然後事實上objectA變為了NSKVONotifying_ASClassA的例項物件,執行方法要從這個類的方法列表裡找。(同時蘋果警告我們,通過isa獲取類的型別是不可靠的,通過class方法總是能得到正確的類=_=!!).

更多關於OC物件,類,isa, 屬性, 變數, 方法的介紹請參考我的另一篇博文 Runtime簡介

思考

由於研究方法有限,並不能知道被觀察者的值改變後,以何種方式去通知觀察者,並使其執行實現的對應的方法的,我們可以猜想,也許是蘋果慣用的,像維護物件們的引用計數,和weak修飾的物件的存亡 一樣,建立了一張hash表去對應觀察者,被觀察者的地址或其他。能力有限,尚不能得知。

謝謝觀看

歡迎大家指出文中的錯誤!

演示程式碼地址:https://github.com/Assuner-Lee/KVC-KVO-Test.git

如有任何智慧財產權、版權問題或理論錯誤,還請指正。

轉載請註明原作者及以上資訊。

相關文章