iOS - KVC底層應用

之前簡單得講過一些KVC的用法,但是並不能深入理解KVC內部實現及其原理,下面主要講下KVC的底層原理。

取值 valueForKey:

在使用KVC取值的時候,使用valueForKey:方法,該方法會返回一個id類型的對象,那麼它的內部會怎麼處理的呢?現在我們使用該方法:

    Teacher *teacher = [[Teacher alloc] init];
    NSString *name = [teacher valueForKey:@"name"];    
    NSLog(@"%@",name);

分析:當使用KVC獲取成員變量的值時,其內部會首先去找getter方法- (NSString *)getName,如果有該方法直接調用;如果沒有就去找getter方法- (NSString *)name,如果有直接調用;如果沒有就去找getter方法- (NSString *)getIsName,如果有直接調用;如果沒有就去找getter方法:- (NSString *)isName,如果有直接調用;如果沒有這裏就會出現轉折,程序不會再去找getter方法,而是去找下面這兩個方法:

// 數組元素數
- (NSInteger)countOfName {
    return 5;
}
// 數組內容
- (id)objectInNameAtIndex:(NSUInteger)index {
    if (index == 0) {
        return @"Michael";
    }
    return @"Tom";
}

如果找到了這個兩個方法,就會返回一個數組類型:
在這裏插入圖片描述
這裏的場景雖然還沒有遇到過,但是KVC內部確實是在沒有訪問到getter方法的時候,會訪問這個兩個方法,如果有直接返回;如果沒有就開始訪問成員變,在訪問成員變量之前會有個判斷方法:+ (BOOL)accessInstanceVariablesDirectly,該方法默認是YES,如果重寫該方法並返回NO,那麼將失去訪問成員變量的權限,也就是說在沒找到getter方法,也沒找到數組方法的時候程序就會異常:valueForUndefinedKey:。下面討論在accessInstanceVariablesDirectly:返回YES的情況,開始去找成員變量,具體用法如下:

// Teacher.h
@interface Teacher : NSObject
{
    NSString * _name;
    NSString * _isName;
    NSString * name;
    NSString * isName;
}
@end

// Teacher.m
@implementation Teacher

- (instancetype)init {
    if (self = [super init]) {
        _name = @"_name";
        _isName = @"_isName";
        name = @"name";
        isName = @"isName";
    }
    return self;
}

@end

上例中在Teacher類中定義了四個成員變量,然後使用KVC取出成員變量name的值,會打輸出什麼呢?輸出結果是:@"_name";如果將成員變量_name及其初始化值註釋,輸出又會如何呢?輸出結果是:@"_isName"…以此類推。結論很出人意料,雖然使用KVC獲取的是成員變量name的值,但是卻可以獲取到四種值,它首先會到對象的成員變量中尋找名爲“_key”的成員變量,如果有就取出值;如果沒有就去找名爲“_isKey”的成員變量,如果有就取出值;如果沒有就去找名爲“key”的成員變量,如果有就取出值;如果沒有就去找名爲“isKey”的成員變量,如果有就取出值;如果沒有程序會crash,報錯原因是:
valueForUndefinedKey:

reason: '[<Teacher 0x60000118fd50> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'

那麼如果我們不想讓程序crash,可以重寫valueForUndefinedKey:方法:

- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}

此時即使key不存在,會返回null,而不會異常。

設值 setValue: forKey:

前面說使用valueForKey:方法獲取值,現在說使用setValue: forKey:設值,那麼該方法底層會怎麼執行呢?首先回去找setter方法:

- (void)setName:(NSString *)name {
    NSLog(@"set name");
}

如果找到沒找到setName:,就會去找:

- (void)setIsName:(NSString *)name {
    NSLog(@"set isName");
}

如果沒找setIsName:方法,這裏不會再去找別的setter方法,而是去查找成員變量,在查找之前還是需要看看accessInstanceVariablesDirectly:的返回值,如果返回NO,則失去訪問成員變量的權限,程序creash;如果返回YES,就去查找成員變量,訪問成員變量的優先級和valueForKey:方法一樣(_key > _isKey > key > isKey),如果在查找成員變量時也沒有找到key,此時程序crash。
爲了避免crash,我們重寫setValue: forUndefinedKey:方法:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"沒有找到這個%@",key);
}

注意:如果成員變量是基礎數據類型(int、float等),在使用setValue: forKey:設值時,不能設值爲nil,不然程序crash,異常原因setNilValueForKey

reason: '[<Teacher 0x6000021b72a0> setNilValueForKey]: could not set nil as the value for the key age.'

這時我們可以重寫setNilValueForKey方法,可以避免這種異常:

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能將 %@ 設置爲nil",key);
}

我們會注意到valueForKey:方法返回值是id類型的,如果我們成員變量是基礎數據類型而不是對象類型,這該如何處理呢?對於KVC,Cocoa會自動裝箱和開箱標量值,在取值的時候其實返回的是NSNumber類型。

監聽數組 mutableArrayValueForKey:

當數組受到KVO監聽時,如果向數組裏面添加元素會被監聽到嗎?例如:

[_teacher addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"new value %@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 監聽數組的變化
    [_teacher.array addObject:@"1"];
}
- (void)dealloc {
    [_teacher removeObserver:self forKeyPath:@"array"];
}

當向數組裏面添加元素時,顯然沒有被監聽到,這時候需要用到KVC的mutableArrayValueForKey:方法,該方法返回一個可變數組,然後再向裏面添加元素:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 監聽數組的變化
    //[_teacher.array addObject:@"1"];
     static int i = 0;
    [[_teacher mutableArrayValueForKey:@"array"] addObject:@(i++)];
    NSLog(@"%@",[_teacher mutableArrayValueForKey:@"array"]);
}

KVC的快速運算

先看一個例子:

    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i < 10; i++) {
        Teacher *t = [[Teacher alloc] init];
        t.age = i;
        [array addObject:t];
    }

利用KVC打印數組的個數:

NSLog(@"%@",[array valueForKey:@"@count"]);

使用@count運算符來求出數組個數,還有一些常見操作符

  • @sum:求和運算符;
  • @min:求最小值運算符;
  • @max:求最大值運算符;
  • @avg:求平均數運算符。
    NSLog(@"%@",[array valueForKeyPath:@"@sum.age"]);//求和
    NSLog(@"%@",[array valueForKeyPath:@"@min.age"]);//最小值
    NSLog(@"%@",[array valueForKeyPath:@"@max.age"]);//最大值
    NSLog(@"%@",[array valueForKeyPath:@"@avg.age"]);//平均值
    //獲取到所有age的一個集合(非數組);特點:無序、不重複
    NSLog(@"%@",[array valueForKeyPath:@"@distinctUnionOfObjects.age"]);

注意:這裏使用的是valueForKeyPath:方法,表示鍵路徑,可以在對象和不同變量之間用圓點隔開,這些鍵路徑的深度是任意的,具體取決於對象的複雜度,常用於對象的符合和快速運算,例如一個Person類中複合一個Teacher類,在Teacher類中再複合一個Student類,這時利用KVC取Student類中的一個成員變量的值,可以表示爲:

NSString *name = [person valueForKeyPath:@"teacher.student.name"];

同樣的還有setValue: forKeyPath:修改成員變量值的方法:

[person setValue:@"John" forKeyPath:@"teacher.student.name"];

簡單實現 setValue: forKey:

下面自己簡單的實現以下setValue: forKey:valueForKey:方法,創建一個NSObject的分類:

-(void)YZ_setValue:(id)value forKey:(NSString *)key
{
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 尋找setKey方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
        [self performSelector:NSSelectorFromString(setKey) withObject:value];
        return;
    }
    
    // 尋找setIsKey方法
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
        [self performSelector:NSSelectorFromString(setIsKey) withObject:value];
        return;
    }
    
    // setter方法都沒有找到去找成員變量
    // 先判斷訪問成員變量權限
    if ([[self class] accessInstanceVariablesDirectly]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        
        // _key
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成員變量名稱
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
                // 給成員變量賦值
                object_setIvar(self, ivar, value);
                return;
            }
        }
        // _isKey
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成員變量名稱
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_Is%@",key.capitalizedString]]) {
                // 給成員變量賦值
                object_setIvar(self, ivar, value);
                return;
            }
        }
        // key
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成員變量名稱
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:key]) {
                // 給成員變量賦值
                object_setIvar(self, ivar, value);
                return;
            }
        }
        // isKey
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成員變量名稱
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
                // 給成員變量賦值
                object_setIvar(self, ivar, value);
                return;
            } 
        }
        
        // 釋放(new copy create)
        free(ivars);
        
    } else {
        // 不允許訪問成員變量,拋出異常
        NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
        @throw exception;
        return;
    }
    
}


- (id)YZ_valueForKey:(NSString *)key {
    
    if (key == nil || key.length == 0) {
        return nil;
    }
    
    // 先找getter方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }
    
    if ([self respondsToSelector:NSSelectorFromString(key)]) {
        return [self performSelector:NSSelectorFromString(key)];
    }
    
    NSString *getIsKey = [NSString stringWithFormat:@"getIs%@",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(getIsKey)]) {
        return [self performSelector:NSSelectorFromString(getIsKey)];
    }
    
    // 沒有查找到getter方法
    NSString *countName = [NSString stringWithFormat:@"countOf%@",key.capitalizedString];
    NSString *objectInName = [NSString stringWithFormat:@"objectIn%@AtIndex:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(countName)] && [self respondsToSelector:NSSelectorFromString(objectInName)]) {
        NSMutableArray *array = [NSMutableArray array];
        NSInteger count = [self performSelector:NSSelectorFromString(countName)];
        for (NSInteger i = 0; i < count; i++) {
             // 這裏好像有點問題
            id objc = [self performSelector:NSSelectorFromString(objectInName) withObject:@(i)];
            NSLog(@"%@ %d",objc,i);
            [array addObject:objc];
        }
        return [NSArray arrayWithArray:array];
    }
    
    // 去找成員變量
    if ([[self class] accessInstanceVariablesDirectly]) {
        // 優先級與YZ_setValue: forKey: 一致
        unsigned int outCount = 0;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        
        // _key
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
                return object_getIvar(self, ivar);
            }
        }
        
        // _isKey
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_is%@",key.capitalizedString]]) {
                return object_getIvar(self, ivar);
            }
        }
        
        // key
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:key]) {
                return object_getIvar(self, ivar);
            }
        }
        
        // isKey
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
                return object_getIvar(self, ivar);
            }
        }
        
        free(ivars);
    } else {
        NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
        @throw exception;
        return nil;
    }
    
    return nil;
}

調用:

    Teacher *teacher = [[Teacher alloc] init];
    [teacher YZ_setValue:@"John" forKey:@"name"];    
    NSLog(@"%@",[teacher YZ_valueForKey:@"name"]);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章