天天看點

【iOS底層】19:KVC分析

前言:本文章開始分析KVC相關~

一、Apple Developer Documentation

位址:傳送門

【iOS底層】19:KVC分析

 這裡就可以查找到kvc相關的蘋果官方文檔↓

【iOS底層】19:KVC分析
【iOS底層】19:KVC分析

二、KVC的設定

【iOS底層】19:KVC分析

來結合代碼了解一下kvc的setter

  • 1.看是否有set<Key> or _set<Key>
- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 設定值的過程 setValue 分析調用過程
     [person setValue:@"Mcl" forKey:@"name"];
}
           

 LGPerson.m中實作

- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
           

 輸出:

-[LGPerson setName:] - Mcl
           

 注釋set<Key> 打開_set<Key>

//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
           

輸出: 

-[LGPerson _setName:] - Mcl
           

setName和_setName都打開注釋,輸出看看

-[LGPerson setName:] - Mcl
           

可以看出set<Key>優先級大于_set<Key>,也印證了文檔中第一條↓

1.查找第一個名為set<Key>:或_set<Key>的通路器,按此順序。如果找到,則使用輸入值(或根據需要取消包裝的值)調用它并完成
  • 2.沒有簡單通路器如何執行

    2.1 檢視類方法

    accessInstanceVariablesDirectly

     的傳回值是否為YES。

    2.2 如果傳回YES,依次查找執行個體變量_<key>, _is< key>, <key>,或is< key>。

結合代碼看下

LGPerson.m中↓ 

/* Return YES if -valueForKey:, -setValue:forKey:, -mutableArrayValueForKey:,
 -storedValueForKey:, -takeStoredValue:forKey:, and -takeValue:forKey: 
may directly manipulate instance variables when sent to instances of the 
receiving class, NO otherwise. The default implementation of this property 
returns YES.
*/
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}
           

其實此方法可以不寫,因為注釋裡寫了預設為YES。

@interface LGPerson : NSObject{
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;

}
           

我們按順序定義對應的4個成員變量。在viewController裡的viewDidLoad方法中按順序依次列印各個成員變量的值。

NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
列印結果:
Mcl-(null)-(null)-(null)
           

去掉LGPerson.h中的成員變量_name:

NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
列印結果:
Mcl-(null)-(null)
           

去掉LGPerson.h中的成員變量_isName:

NSLog(@"%@-%@",person->name,person->isName);
列印結果:
Mcl-(null)
           

最後去掉LGPerson.h中的成員變量name: 

NSLog(@"%@",person->isName);
列印結果:
Mcl
           
  • 這裡有個小疑問,既然第一步會找

    set<Key>:

     or 

    _set<Key>,第二部多了2個帶'is'的成員變量,那麼會不會找setIs<Key>和_setIs<Key>呢?

嘗試下在LGPerson實作類裡實作以下2個方法

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

// 沒有調用
- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
           

同時打開4個成員變量的注釋

NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
           

viewController中

[person setValue:@"Mcl" forKey:@"name"];

NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
           

運作檢視列印結果 :

隻打開setIsName:方法
-[LGPerson setIsName:] - Mcl
(null)-(null)-(null)-(null)

隻打開_setIsName:方法
Mcl-(null)-(null)-(null)

           

發現并沒有走 _setIsName:方法,是以實際上可以通過3種方法指派,而不是文檔裡寫的2種。

是以:雖然沒有寫- (void)setIsName:(NSString *)name 和 - (void)_setIsName:(NSString *)name

但是當我們實作了- (void)setIsName:(NSString *)name之後,列印4個成員變量的值都是空,說明生效了,并且優先級高于4個成員變量。

簡單總結就是:

2.如果沒有找到簡單的通路器,并且如果類方法accessinstancevariablesdirect傳回YES,請查找一個執行個體變量,其名稱依次為_<key>, _is< key>, <key>,或is< key>。如果找到,直接用輸入值(或取消包裝的值)設定變量并完成。
  • 3. 
在沒有找到通路器或執行個體變量時,調用setValue:forUndefinedKey:。預設情況下,這會引發一個異常,但是NSObject的子類可能提供特定于鍵的行為。

嘗試之後發現4個成員變量隻要存在任意一個就會正常列印,都注釋掉就會報錯。

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<LGPerson 0x600001a5bd80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'
           

三、KVC的取值

【iOS底層】19:KVC分析

KVC的取值共有6點。

  • 1.
在執行個體中搜尋第一個名稱為get<Key>、<Key>、is<Key>或_< Key>的通路器方法。如果找到了,就調用它,并使用結果繼續步驟5。否則執行下一步。

代碼驗證:

//viewController中viewDidLoad代碼:
LGPerson *person = [[LGPerson alloc] init];

//分别指派,便于區分
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";

//這裡打斷點,輸出檢視
NSLog(@"取值:%@",[person valueForKey:@"name"]);
           
//LGPerson.m中加入下列代碼:
- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}
           
 _cmd是隐藏的參數,代表目前方法的selector。

上面4個方法都打開時,輸出結果為

取值:getName
           

依次注釋掉4個方法得到結論:

确實是按照蘋果文檔裡的順序取值的,而且如果4個方法都注釋掉,預設輸出代下劃線的_<Key>。

  • 2.

如果沒有找到簡單的通路器方法,在執行個體中搜尋比對模式countOf<Key>和objectIn<Key>AtIndex:(對應于NSArray類定義的原始方法)和<Key> AtIndexes:(對應于NSArray方法objectsAtIndexes:)的方法。

如果找到了第一個和另外兩個中的至少一個,建立一個集合代理對象,響應所有NSArray方法并傳回它。否則,執行步驟3。

代理對象随後将它接收到的所有NSArray消息轉換為countOf<Key>, objectIn<Key>AtIndex:,和<Key> AtIndexes:消息的組合,并将其轉換為符合鍵值編碼的建立它的對象。如果原始對象還實作了一個名稱為get<Key>:range:的可選方法,那麼代理對象也會在适當的時候使用該方法。實際上,代理對象與符合鍵值編碼的對象一起工作,允許底層屬性像NSArray一樣行為,即使它不是NSArray。

  • 3.

如果沒有找到簡單的通路方法或數組通路方法組,則查找名為countOf<Key>, enumeratorOf<Key>,和memberOf<Key>:(對應于NSSet類定義的基本方法)的三個方法。

如果找到了所有三個方法,建立一個集合代理對象響應所有的NSSet方法并傳回它。否則,執行步驟4。

這個代理對象随後将它接收到的任何NSSet消息轉換為countOf<Key>, enumeratorOf<Key>, memberOf<Key>: messages的組合,并将其轉換為建立它的對象。實際上,代理對象與符合鍵值編碼的對象一起工作,允許底層屬性的行為就像它是一個NSSet,即使它不是。

  • 4.
如果沒有找到簡單的通路方法或一組集合通路方法,并且如果接收方的類方法accessinstancevariablesdirect傳回YES,則搜尋執行個體變量_<key>, _is< key>, <key>,或is< key>,依次。如果找到,直接擷取執行個體變量的值,然後繼續步驟5。否則,執行步驟6。
  • 5.

如果檢索到的屬性值是對象指針,隻需傳回結果。

如果該值是NSNumber支援的标量類型,則将其存儲在NSNumber執行個體中并傳回該執行個體。

如果結果是NSNumber不支援的标量類型,則轉換為NSValue對象并傳回該對象。

  • 6.
如果其他方法都失敗了,調用valueForUndefinedKey:。預設情況下,這會引發一個異常,但是NSObject的子類可能提供特定于鍵的行為。

這裡暫時不做舉例結合分析了。

四、KVC自定義實作

  • set實作

大緻流程如下:

1.首先判斷key是否為空。

2.

        2.1判斷accessInstanceVariablesDirectly類方法是否為YES。

        2.2依次查找4個執行個體變量 

_<key>

_is<Key>

<key>

, or 

is<Key>。

3.抛出異常。

代碼:

建立一個NSObject的分類

@interface NSObject (LGKVC)

// LG KVC 自定義入口
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
@end

@implementation NSObject (LGKVC)

- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    // 1: 判斷什麼 key
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set<Key>: or _set<Key>,
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判斷是否響應 accessInstanceVariablesDirectly 傳回YES NO 奔潰
    // 3:判斷是否能夠直接指派執行個體變量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

    // 4: 間接變量
    // 擷取 ivar -> 周遊 containsObjct -
    // 4.1 定義一個收集執行個體變量的可變數組
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 擷取相應的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 對相應的 ivar 設定值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    
    // 5:如果找不到相關執行個體
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

#pragma mark - 相關方法
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}
@end
           
  • get實作

類似set方法步驟按照文檔流程寫get實作 

- (nullable id)lg_valueForKey:(NSString *)key{
    
    // 1:刷選key 判斷非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相關方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 3:判斷是否能夠直接指派執行個體變量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相關執行個體變量進行指派
    // 4.1 定義一個收集執行個體變量的可變數組
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}

#pragma mark - 相關方法
- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}
           

總結、流程圖

【iOS底層】19:KVC分析

繼續閱讀