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

2017-03-11 | Assuner | iOS

概述

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

演示代碼地址:https://github.com/Assuner-Lee/KVC-KVO-Test.gitgit

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方法,②⑤⑨直接訪問的實例變量。github

緣由

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

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

簡而言之:app

1.訪問器匹配:先尋找與key,isKey, getKey (實測還有_key)同名的方法,返回值爲對象類型。ide

2.實例變量匹配:尋找與key, _key,isKey,_isKey同名的實例變量ui

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

簡而言之:this

1.存取器匹配:先尋找與setKey同名的方法,且參數要爲一個對象類型atom

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

其餘

當咱們使用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

複製代碼

運行結果

運行結果

分析以上結果

咱們經過抓取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

若有任何知識產權、版權問題或理論錯誤,還請指正。

轉載請註明原做者及以上信息。

相關文章
相關標籤/搜索