iOS KVC和KVO詳解

KVC

KVC定義

KVC(Key-value coding)鍵值編碼,就是指iOS的開發中,能夠容許開發者經過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不須要調用明確的存取方法。這樣就能夠在運行時動態地訪問和修改對象的屬性。而不是在編譯時肯定,這也是iOS開發中的黑魔法之一。不少高級的iOS開發技巧都是基於KVC實現的。html

在實現了訪問器方法的類中,使用點語法和KVC訪問對象其實差異不大,兩者能夠任意混用。可是沒有訪問起方法的類中,點語法沒法使用,這時KVC就有優點了。編程

KVC的定義都是對NSObject的擴展來實現的,Objective-C中有個顯式的NSKeyValueCoding類別名,因此對於全部繼承了NSObject的類型,都能使用KVC(一些純Swift類和結構體是不支持KVC的,由於沒有繼承NSObject),下面是KVC最爲重要的四個方法:api

- (nullable id)valueForKey:(NSString *)key;                          //直接經過Key來取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;          //經過Key來設值

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //經過KeyPath來取值

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //經過KeyPath來設值
複製代碼

NSKeyValueCoding類別中其餘的一些方法:數組

+ (BOOL)accessInstanceVariablesDirectly;
//默認返回YES,表示若是沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性�驗證的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個方法同樣,但這個方法是設值。

- (void)setNilValueForKey:(NSString *)key;
//若是你在SetValue方法時面給Value傳nil,則會調用這個方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
複製代碼

同時蘋果對一些容器類好比NSArray或者NSSet等,KVC有着特殊的實現。安全

有序集合對應方法以下:bash

-countOf<Key>//必須實現,對應於NSArray的基本方法count:2  -objectIn<Key>AtIndex:

-<key>AtIndexes://這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實現的,但實現後能夠提升性能,其對應於 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://可選的,若是在此類操做上有性能問題,就須要考慮實現之
複製代碼

無序集合對應方法以下:微信

-countOf<Key>//必須實現,對應於NSArray的基本方法count:

-objectIn<Key>AtIndex:

-<key>AtIndexes://這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實現的,但實現後能夠提升性能,其對應於 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://這兩個都是可選的,若是在此類操做上有性能問題,就須要考慮實現之
複製代碼

經過如下幾個方面講解KVC相關的技術概念以及使用:多線程

  • KVC設值
  • KVC取值
  • KVC使用keyPath
  • KVC處理異常
  • KVC處理數值和結構體類型屬性
  • KVC鍵值驗證(Key-Value Validation)
  • KVC處理集合
  • KVC處理字典

KVC相關技術概念

KVC設值

KVC要設值,那麼就要對象中對應的key,KVC在內部是按什麼樣的順序來尋找key的。當調用setValue:屬性值 forKey:@」name「的代碼時,底層的執行機制以下:app

  • 程序優先調用set:屬性值方法,代碼經過setter方法完成設置。注意,這裏的是指成員變量名,首字母大小寫要符合KVC的命名規則,下同ide

  • 若是沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,若是你重寫了該方法讓其返回NO的話,那麼在這一步KVC會執行setValue:forUndefinedKey:方法,不過通常開發者不會這麼作。因此KVC機制會搜索該類裏面有沒有名爲_的成員變量,不管該變量是在類接口處定義,仍是在類實現處定義,也不管用了什麼樣的訪問修飾符,只在存在以_命名的變量,KVC均可以對該成員變量賦值。

  • 若是該類即沒有set:方法,也沒有_成員變量,KVC機制會搜索_is的成員變量。

  • 和上面同樣,若是該類即沒有set:方法,也沒有_和_is成員變量,KVC機制再會繼續搜索和is的成員變量。再給它們賦值。

  • 若是上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。

簡單來講就是若是沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員並進行賦值操做

若是開發者想讓這個類禁用KVC裏,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO便可,這樣的話若是KVC沒有找到set:屬性名時,會直接用setValue:forUndefinedKey:方法。

下面看例子:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_name;
}

@end

@implementation Test

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //經過KVC賦值name
        [obj setValue:@"xiaoming" forKey:@"name"];
        //經過KVC取值name打印
        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 15:36:52.354405+0800 KVCKVO[35231:6116188] obj的名字是xiaoming

能夠看到經過- (void)setValue:(nullable id)value forKey:(NSString *)key;- (nullable id)valueForKey:(NSString *)key;成功設置和取出obj對象的name值。

再看一下設置accessInstanceVariablesDirectly爲NO的效果:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_name;
}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //經過KVC賦值name
        [obj setValue:@"xiaoming" forKey:@"name"];
        //經過KVC取值name打印
        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 15:45:22.399021+0800 KVCKVO[35290:6145826] 出現異常,該key不存在name 2018-05-05 15:45:22.399546+0800 KVCKVO[35290:6145826] 出現異常,該key不存在name 2018-05-05 15:45:22.399577+0800 KVCKVO[35290:6145826] obj的名字是(null)

能夠看到accessInstanceVariablesDirectly爲NO的時候KVC只會查詢setter和getter這一層,下面尋找key的相關變量執行就會中止,直接報錯。

設置accessInstanceVariablesDirectly爲YES,再修改_name爲_isName,看看執行是否成功。

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSString *_isName;
}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //經過KVC賦值name
        [obj setValue:@"xiaoming" forKey:@"name"];
        //經過KVC取值name打印
        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 15:49:53.444350+0800 KVCKVO[35303:6157671] obj的名字是xiaoming

從打印能夠看到設置accessInstanceVariablesDirectly爲YES,KVC會繼續按照順序查找,併成功設值和取值了。

KVC取值

當調用valueForKey:@」name「的代碼時,KVC對key的搜索方式不一樣於setValue:屬性值 forKey:@」name「,其搜索方式以下:

  • 首先按get,,is的順序方法查找getter方法,找到的話會直接調用。若是是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。

  • 若是上面的getter沒有找到,KVC則會查找countOf,objectInAtIndex或AtIndexes格式的方法。若是countOf方法和另外兩個方法中的一個被找到,那麼就會返回一個能夠響應NSArray全部方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬於NSArray的方法,就會以countOf,objectInAtIndex或AtIndexes這幾個方法組合的形式調用。還有一個可選的get:range:方法。因此你想從新定義KVC的一些功能,你能夠添加這些方法,須要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。

  • 若是上面的方法沒有找到,那麼會同時查找countOf,enumeratorOf,memberOf格式的方法。若是這三個方法都找到,那麼就返回一個能夠響應NSSet所的方法的代理集合,和上面同樣,給這個代理集合發NSSet的消息,就會以countOf,enumeratorOf,memberOf組合的形式調用。

  • 若是尚未找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,若是返回YES(默認行爲),那麼和先前的設值同樣,會按_,_is,,is的順序搜索成員變量名,這裏不推薦這麼作,由於這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。若是重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那麼會直接調用valueForUndefinedKey:方法,默認是拋出異常。

給Test類添加getAge方法,例如以下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)getAge {
    return 10;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //經過KVC取值age打印
        NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:00:04.207857+0800 KVCKVO[35324:6188613] obj的年齡是10

能夠看到[obj valueForKey:@"age"],找到了getAge方法,而且取到了值。

下面把getAge改爲age,例子以下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)age {
    return 10;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //經過KVC取值age打印
        NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:02:27.270954+0800 KVCKVO[35337:6195086] obj的年齡是10

能夠看到[obj valueForKey:@"age"],找到了age方法,而且取到了值。

下面把getAge改爲isAge,例子以下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (NSUInteger)isAge {
    return 10;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        Test *obj = [[Test alloc] init];
        //經過KVC取值age打印
        NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:03:56.234338+0800 KVCKVO[35345:6201242] obj的年齡是10

能夠看到[obj valueForKey:@"age"],找到了isAge方法,而且取到了值。

上面的代碼說明了說明了KVC在調用valueforKey:@"age"時搜索key的機制。

KVC使用keyPath

在開發過程當中,一個類的成員變量有多是自定義類或其餘的複雜數據類型,你能夠先用KVC獲取該屬性,而後再次用KVC來獲取這個自定義類的屬性, 但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath。顧名思義,就是按照路徑尋找key。

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //經過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //經過KeyPath來設值
複製代碼

用代碼實現以下:

#import <Foundation/Foundation.h>

@interface Test1: NSObject {
    NSString *_name;
}
@end

@implementation Test1
@end

@interface Test: NSObject {
    Test1 *_test1;
}

@end

@implementation Test
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //Test1生成對象
        Test1 *test1 = [[Test1 alloc] init];
        //經過KVC設值test"test1"
        [test setValue:test1 forKey:@"test1"];
        //經過KVC設值test"test1的name"
        [test setValue:@"xiaoming" forKeyPath:@"test1.name"];
        //經過KVC取值age打印
        NSLog(@"test的\"test1的name\"是%@", [test valueForKeyPath:@"test1.name"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:19:02.613394+0800 KVCKVO[35436:6239788] test的"test1的name"是xiaoming

從打印結果來看咱們成功的經過keyPath設置了test1的值。 KVC對於keyPath是搜索機制第一步就是分離key,用小數點.來分割key,而後再像普通key同樣按照先前介紹的順序搜索下去。

KVC處理異常

KVC中最多見的異常就是不當心使用了錯誤的key,或者在設值中不當心傳遞了nil的值,KVC中有專門的方法來處理這些異常。

KVC處理nil異常

一般狀況下,KVC不容許你要在調用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值。很簡單,由於值類型是不能爲nil的。若是你不當心傳了,KVC會調用setNilValueForKey:方法。這個方法默認是拋出異常,因此通常而言最好仍是重寫這個方法。

代碼實現以下:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger age;
}

@end

@implementation Test

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

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //經過KVC設值test的age
        [test setValue:nil forKey:@"age"];
        //經過KVC取值age打印
        NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:24:30.302134+0800 KVCKVO[35470:6258307] 不能將age設成nil 2018-05-05 16:24:30.302738+0800 KVCKVO[35470:6258307] test的年齡是0

KVC處理UndefinedKey異常

一般狀況下,KVC不容許你要在調用setValue:屬性值 forKey:(或者keyPath)時對不存在的key進行操做。 否則,會報錯forUndefinedKey發生崩潰,重寫forUndefinedKey方法避免崩潰。

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //經過KVC設值test的age
        [test setValue:@10 forKey:@"age"];
        //經過KVC取值age打印
        NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:30:18.564680+0800 KVCKVO[35487:6277523] 出現異常,該key不存在age 2018-05-05 16:30:18.565190+0800 KVCKVO[35487:6277523] 出現異常,該key不存在age 2018-05-05 16:30:18.565216+0800 KVCKVO[35487:6277523] test的年齡是(null)

KVC處理數值和結構體類型屬性

不是每個方法都返回對象,可是valueForKey:老是返回一個id對象,若是本來的變量類型是值類型或者結構體,返回值會封裝成NSNumber或者NSValue對象。 這兩個類會處理從數字,布爾值到指針和結構體任何類型。而後開以者須要手動轉換成原來的類型。 儘管valueForKey:會自動將值類型封裝成對象,可是setValue:forKey:卻不行。你必須手動將值類型轉換成NSNumber或者NSValue類型,才能傳遞過去。 由於傳遞進去和取出來的都是id類型,因此須要開發者本身擔保類型的正確性,運行時Objective-C在發送消息的會檢查類型,若是錯誤會直接拋出異常。

舉個例子,Person類有個NSInteger類型的age屬性,以下:

//  Person.m
#import "Person.h"
 
@interface Person ()
 
@property (nonatomic,assign) NSInteger age;
 
@end
 
 
@implementation Person
 
@end
複製代碼

修改值

咱們經過KVC技術使用以下方式設置age屬性的值:

[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
複製代碼

咱們賦給age的是一個NSNumber對象,KVC會自動的將NSNumber對象轉換成NSInteger對象,而後再調用相應的訪問器方法設置age的值。

獲取值

一樣,以以下方式獲取age屬性值:

[person valueForKey:@"age"];
複製代碼

這時,會以NSNumber的形式返回age的值。

//  ViewController.m
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc]init];
    [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
    NSLog(@"age=%@",[person valueForKey:@"age"]);
    
}


@end
複製代碼

打印結果: 2017-01-16 16:31:55.709 Test[28586:2294177] age=5

須要注意的是咱們不能直接將一個數值經過KVC賦值的,咱們須要把數據轉爲NSNumber和NSValue類型傳入,那到底哪些類型數據要用NSNumber封裝哪些類型數據要用NSValue封裝呢?看下面這些方法的參數類型就知道了:

可使用NSNumber的數據類型有:

+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value;
+ (NSNumber*)numberWithDouble:(double)value;
+ (NSNumber*)numberWithBool:(BOOL)value;
+ (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);
複製代碼

就是一些常見的數值型數據。

可使用NSValue的數據類型有:

+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);
複製代碼

NSValue主要用於處理結構體型的數據,它自己提供瞭如上集中結構的支持。任何結構體都是能夠轉化成NSValue對象的,包括其它自定義的結構體。

KVC鍵值驗證(Key-Value Validation)

KVC提供了驗證Key對應的Value是否可用的方法:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;
複製代碼

該方法默認的實現是調用一個以下格式的方法:

- (BOOL)validate<Key>:error:
複製代碼

例如:

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger _age;
}

@end

@implementation Test

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue == 10) {
        return NO;
    }
    
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成對象
        Test *test = [[Test alloc] init];
        //經過KVC設值test的age
        NSNumber *age = @10;
        NSError* error;
        NSString *key = @"age";
        BOOL isValid = [test validateValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"鍵值匹配");
            [test setValue:age forKey:key];
        }
        else {
            NSLog(@"鍵值不匹配");
        }
        //經過KVC取值age打印
        NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 16:59:06.111671+0800 KVCKVO[35777:6329982] 鍵值不匹配 2018-05-05 16:59:06.112215+0800 KVCKVO[35777:6329982] test的年齡是0

這樣就給了咱們一次糾錯的機會。須要指出的是,KVC是不會自動調用鍵值驗證方法的,就是說咱們若是想要鍵值驗證則須要手動驗證。可是有些技術,好比CoreData會自動調用。

KVC處理集合

KVC同時還提供了很複雜的函數,主要有下面這些:

簡單集合運算符

簡單集合運算符共有@avg, @count , @max , @min ,@sum5種,都表示什麼直接看下面例子就明白了, 目前還不支持自定義。

#import <Foundation/Foundation.h>

@interface Book : NSObject
@property (nonatomic, copy)  NSString* name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 10;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 40;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
        NSLog(@"sum:%f",sum.floatValue);
        NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSLog(@"avg:%f",avg.floatValue);
        NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
        NSLog(@"count:%f",count.floatValue);
        NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
        NSLog(@"min:%f",min.floatValue);
        NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
        NSLog(@"max:%f",max.floatValue);
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 17:04:50.674243+0800 KVCKVO[35785:6351239] sum:100.000000 2018-05-05 17:04:50.675007+0800 KVCKVO[35785:6351239] avg:25.000000 2018-05-05 17:04:50.675081+0800 KVCKVO[35785:6351239] count:4.000000 2018-05-05 17:04:50.675146+0800 KVCKVO[35785:6351239] min:10.000000 2018-05-05 17:04:50.675204+0800 KVCKVO[35785:6351239] max:40.000000

對象運算符

比集合運算符稍微複雜,能以數組的方式返回指定的內容,一共有兩種:

  • @distinctUnionOfObjects
  • @unionOfObjects

它們的返回值都是NSArray,區別是前者返回的元素都是惟一的,是去重之後的結果;後者返回的元素是全集。

例如:

#import <Foundation/Foundation.h>

@interface Book : NSObject
@property (nonatomic, copy)  NSString* name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Book *book1 = [Book new];
        book1.name = @"The Great Gastby";
        book1.price = 40;
        Book *book2 = [Book new];
        book2.name = @"Time History";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"Wrong Hole";
        book3.price = 30;
        
        Book *book4 = [Book new];
        book4.name = @"Wrong Hole";
        book4.price = 10;
        
        NSArray* arrBooks = @[book1,book2,book3,book4];
        
        NSLog(@"distinctUnionOfObjects");
        NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
        for (NSNumber *price in arrDistinct) {
            NSLog(@"%f",price.floatValue);
        }
        NSLog(@"unionOfObjects");
        NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
        for (NSNumber *price in arrUnion) {
            NSLog(@"%f",price.floatValue);
        }
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 17:06:21.832401+0800 KVCKVO[35798:6358293] distinctUnionOfObjects 2018-05-05 17:06:21.833079+0800 KVCKVO[35798:6358293] 10.000000 2018-05-05 17:06:21.833112+0800 KVCKVO[35798:6358293] 20.000000 2018-05-05 17:06:21.833130+0800 KVCKVO[35798:6358293] 30.000000 2018-05-05 17:06:21.833146+0800 KVCKVO[35798:6358293] 40.000000 2018-05-05 17:06:21.833165+0800 KVCKVO[35798:6358293] unionOfObjects 2018-05-05 17:06:21.833297+0800 KVCKVO[35798:6358293] 40.000000 2018-05-05 17:06:21.833347+0800 KVCKVO[35798:6358293] 20.000000 2018-05-05 17:06:21.833371+0800 KVCKVO[35798:6358293] 30.000000 2018-05-05 17:06:21.833388+0800 KVCKVO[35798:6358293] 10.000000

KVC處理字典

當對NSDictionary對象使用KVC時,valueForKey:的表現行爲和objectForKey:同樣。因此使用valueForKeyPath:用來訪問多層嵌套的字典是比較方便的。

KVC裏面還有兩個關於NSDictionary的方法:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
複製代碼

dictionaryWithValuesForKeys:是指輸入一組key,返回這組key對應的屬性,再組成一個字典。 setValuesForKeysWithDictionary是用來修改Model中對應key的屬性。下面直接用代碼會更直觀一點:

#import <Foundation/Foundation.h>

@interface Address : NSObject

@end

@interface Address()

@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;

@end

@implementation Address

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //模型轉字典
        Address* add = [Address new];
        add.country = @"China";
        add.province = @"Guang Dong";
        add.city = @"Shen Zhen";
        add.district = @"Nan Shan";
        NSArray* arr = @[@"country",@"province",@"city",@"district"];
        NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對應key全部的屬性所有取出來
        NSLog(@"%@",dict);
        
        //字典轉模型
        NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict];            //用key Value來修改Model的屬性
        NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
        
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 17:08:48.824653+0800 KVCKVO[35807:6368235] { city = "Shen Zhen"; country = China; district = "Nan Shan"; province = "Guang Dong"; } 2018-05-05 17:08:48.825075+0800 KVCKVO[35807:6368235] country:USA province:california city:Los angle

打印出來的結果徹底符合預期。

KVC使用

KVC在iOS開發中是毫不可少的利器,這種基於運行時的編程方式極大地提升了靈活性,簡化了代碼,甚至實現不少難以想像的功能,KVC也是許多iOS開發黑魔法的基礎。 下面列舉iOS開發中KVC的使用場景.

動態地取值和設值

利用KVC動態的取值和設值是最基本的用途了。

用KVC來訪問和修改私有變量

對於類裏的私有屬性,Objective-C是沒法直接訪問的,可是KVC是能夠的。

Model和字典轉換

這是KVC強大做用的又一次體現,KVC和Objc的runtime組合能夠很容易的實現Model和字典的轉換。

修改一些控件的內部屬性

這也是iOS開發中必不可少的小技巧。衆所周知不少UI控件都由不少內部UI控件組合而成的,可是Apple度沒有提供這訪問這些控件的API,這樣咱們就沒法正常地訪問和修改這些控件的樣式。 而KVC在大多數狀況可下能夠解決這個問題。最經常使用的就是個性化UITextField中的placeHolderText了。

操做集合

Apple對KVC的valueForKey:方法做了一些特殊的實現,好比說NSArray和NSSet這樣的容器類就實現了這些方法。因此能夠用KVC很方便地操做集合。

用KVC實現高階消息傳遞

當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每個對象,而不是容器自己進行操做。結果會被添加進返回的容器中,這樣,開發者能夠很方便的操做集合來返回另外一個集合。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSArray* arrStr = @[@"english",@"franch",@"chinese"];
        NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
        for (NSString* str  in arrCapStr) {
            NSLog(@"%@",str);
        }
        NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
        for (NSNumber* length  in arrCapStrLength) {
            NSLog(@"%ld",(long)length.integerValue);
        }
        
    }
    return 0;
}
複製代碼

打印結果: 2018-05-05 17:16:21.975983+0800 KVCKVO[35824:6395514] English 2018-05-05 17:16:21.976296+0800 KVCKVO[35824:6395514] Franch 2018-05-05 17:16:21.976312+0800 KVCKVO[35824:6395514] Chinese 2018-05-05 17:16:21.976508+0800 KVCKVO[35824:6395514] 7 2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 6 2018-05-05 17:16:21.976550+0800 KVCKVO[35824:6395514] 7

方法capitalizedString被傳遞到NSArray中的每一項,這樣,NSArray的每一員都會執行capitalizedString並返回一個包含結果的新的NSArray。 從打印結果能夠看出,全部String都成功以轉成了大寫。 一樣若是要執行多個方法也能夠用valueForKeyPath:方法。它先會對每個成員調用 capitalizedString方法,而後再調用length,由於lenth方法返回是一個數字,因此返回結果以NSNumber的形式保存在新數組裏。

實現KVO

KVO是基於KVC實現的,下面講一下KVO的概念和實現。

KVO

KVO定義

KVO 即 Key-Value Observing,翻譯成鍵值觀察。它是一種觀察者模式的衍生。其基本思想是,對目標對象的某屬性添加觀察,當該屬性發生變化時,經過觸發觀察者對象實現的KVO接口方法,來自動的通知觀察者。

觀察者模式是什麼 一個目標對象管理全部依賴於它的觀察者對象,並在它自身的狀態改變時主動通知觀察者對象。這個主動通知一般是經過調用各觀察者對象所提供的接口方法來實現的。觀察者模式較完美地將目標對象與觀察者對象解耦。

簡單來講KVO能夠經過監聽key,來得到value的變化,用來在對象之間監聽狀態變化。KVO的定義都是對NSObject的擴展來實現的,Objective-C中有個顯式的NSKeyValueObserving類別名,因此對於全部繼承了NSObject的類型,都能使用KVO(一些純Swift類和結構體是不支持KVC的,由於沒有繼承NSObject)。

KVO使用

註冊與解除註冊

若是咱們已經有了包含可供鍵值觀察屬性的類,那麼就能夠經過在該類的對象(被觀察對象)上調用名爲 NSKeyValueObserverRegistration 的 category 方法將觀察者對象與被觀察者對象註冊與解除註冊:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
複製代碼
observer:觀察者,也就是KVO通知的訂閱者。訂閱着必須實現 
observeValueForKeyPath:ofObject:change:context:方法
keyPath:描述將要觀察的屬性,相對於被觀察者。
options:KVO的一些屬性配置;有四個選項。
context: 上下文,這個會傳遞到訂閱着的函數中,用來區分消息,因此應當是不一樣的。
複製代碼

options所包括的內容

NSKeyValueObservingOptionNew:change字典包括改變後的值
NSKeyValueObservingOptionOld:change字典包括改變前的值
NSKeyValueObservingOptionInitial:註冊後馬上觸發KVO通知
NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
複製代碼

這兩個方法的定義在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均實現了以上方法,所以咱們不只能夠觀察普通對象,還能夠觀察數組或結合類對象。在該頭文件中,咱們還能夠看到 NSObject 還實現了 NSKeyValueObserverNotification 的 category 方法(更多相似方法,請查看該頭文件NSKeyValueObserving.h):

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
複製代碼

這兩個方法在手動實現鍵值觀察時會用到。注意在不用的時候,不要忘記解除註冊,不然會致使內存泄露。

處理變動通知

每當監聽的keyPath發生變化了,就會在這個函數中回調。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
複製代碼

在這裏,change 這個字典保存了變動信息,具體是哪些信息取決於註冊時的 NSKeyValueObservingOptions。

手動KVO(禁用KVO)

KVO的實現,是對註冊的keyPath中自動實現了兩個函數,在setter中,自動調用。

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
複製代碼

可能有時候,咱們要實現手動的KVO,或者__咱們實現的類庫不但願被KVO__。 這時候須要關閉自動生成KVO通知,而後手動的調用,手動通知的好處就是,能夠靈活加上本身想要的判斷條件。下面看個例子以下:

@interface Target : NSObject
{
    int age;
}

// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

// for manual KVO - age
- (int) age
{
    return age;
}

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

@end
複製代碼

首先,須要手動實現屬性的 setter 方法,並在設置操做的先後分別調用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用於通知系統該 key 的屬性值即將和已經變動了; 其次,要實現類方法 automaticallyNotifiesObserversForKey,並在其中設置對該 key 不自動發送通知(返回 NO 便可)。這裏要注意,對其它非手動實現的 key,要轉交給 super 來處理。 若是須要__禁用該類KVO__的話直接automaticallyNotifiesObserversForKey返回NO,實現屬性的 setter 方法,不進行調用willChangeValueForKey: 和 didChangeValueForKey方法。

鍵值觀察依賴鍵

有時候一個屬性的值依賴於另外一對象中的一個或多個屬性,若是這些屬性中任一屬性的值發生變動,被依賴的屬性值也應當爲其變動進行標記。所以,object 引入了依賴鍵。

觀察依賴鍵

觀察依賴鍵的方式與前面描述的同樣,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加處理變動通知的代碼:

#import "TargetWrapper.h"

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}
複製代碼
實現依賴鍵

在這裏,觀察的是 TargetWrapper 類的 information 屬性,該屬性是依賴於 Target 類的 age 和 grade 屬性。爲此,我在 Target 中添加了 grade 屬性:

@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end

@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
@end
複製代碼

下面來看看 TragetWrapper 中的依賴鍵屬性是如何實現的。

@class Target;

@interface TargetWrapper : NSObject
{
@private
    Target * _target;
}

@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;

-(id) init:(Target *)aTarget;

@end

#import "TargetWrapper.h"
#import "Target.h"

@implementation TargetWrapper

@synthesize target = _target;

-(id) init:(Target *)aTarget
{
    self = [super init];
    if (nil != self) {
        _target = [aTarget retain];
    }
    
    return self;
}

-(void) dealloc
{
    self.target = nil;
    [super dealloc];
}

- (NSString *)information
{
    return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}

- (void)setInformation:(NSString *)theInformation
{
    NSArray * array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

+ (NSSet *)keyPathsForValuesAffectingInformation
{
    NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
    return keyPaths;
}

//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
//    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//    NSArray * moreKeyPaths = nil;
//    
//    if ([key isEqualToString:@"information"])
//    {
//        moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
//    }
//    
//    if (moreKeyPaths)
//    {
//        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
//    }
//    
//    return keyPaths;
//}

@end
複製代碼

首先,要手動實現屬性 information 的 setter/getter 方法,在其中使用 Target 的屬性來完成其 setter 和 getter。

其次,要實現 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統 information 屬性依賴於哪些其餘屬性,這兩個方法都返回一個key-path 的集合。在這裏要多說幾句,若是選擇實現 keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結果 set,而後判斷 key 是否是目標 key,若是是就將依賴屬性的 key-path 結合追加到 super 返回的結果 set 中,不然直接返回 super的結果。

在這裏,information 屬性依賴於 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發生變化,information 的觀察者都會獲得通知。

Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];

TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
          forKeyPath:@"information"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:[TargetWrapper class]];

[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];
複製代碼

打印結果: class: TargetWrapper, Information changed old information is 0#10 new information is 0#30 class: TargetWrapper, Information changed old information is 0#30 new information is 1#30

KVO和線程

一個須要注意的地方是,KVO 行爲是同步的,而且發生與所觀察的值發生變化的一樣的線程上。沒有隊列或者 Run-loop 的處理。手動或者自動調用 -didChange... 會觸發 KVO 通知。

因此,當咱們試圖從其餘線程改變屬性值的時候咱們應當十分當心,除非能肯定全部的觀察者都用線程安全的方法處理 KVO 通知。一般來講,咱們不推薦把 KVO 和多線程混起來。若是咱們要用多個隊列和線程,咱們不該該在它們互相之間用 KVO。

KVO 是同步運行的這個特性很是強大,只要咱們在單一線程上面運行(好比主隊列 main queue),KVO 會保證下列兩種狀況的發生:

首先,若是咱們調用一個支持 KVO 的 setter 方法,以下所示:

self.exchangeRate = 2.345;
複製代碼

KVO 能保證全部 exchangeRate 的觀察者在 setter 方法返回前被通知到。

其次,若是某個鍵被觀察的時候附上了 NSKeyValueObservingOptionPrior 選項,直到 -observe... 被調用以前, exchangeRate 的 accessor 方法都會返回一樣的值。

KVO實現

KVO 是經過 isa-swizzling 實現的。 基本的流程就是編譯器自動爲被觀察對象創造一個派生類,並將被觀察對象的isa 指向這個派生類。若是用戶註冊了對某此目標對象的某一個屬性的觀察,那麼此派生類會重寫這個方法,並在其中添加進行通知的代碼。Objective-C 在發送消息的時候,會經過 isa 指針找到當前對象所屬的類對象。而類對象中保存着當前對象的實例方法,所以在向此對象發送消息時候,其實是發送到了派生類對象的方法。因爲編譯器對派生類的方法進行了 override,並添加了通知代碼,所以會向註冊的對象發送通知。注意派生類只重寫註冊了觀察者的屬性方法。

蘋果官方文檔的說明以下:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

KVO的實現依賴於Runtime的強大動態能力。

即當一個類型爲 ObjectA 的對象,被添加了觀察後,系統會生成一個 NSKVONotifying_ObjectA 類,並將對象的isa指針指向新的類,也就是說這個對象的類型發生了變化。這個類相比較於ObjectA,會重寫如下幾個方法。

重寫setter

在 setter 中,會添加如下兩個方法的調用。

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
複製代碼

而後在 didChangeValueForKey: 中,去調用:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context;
複製代碼

包含了新值和舊值的通知。

因而實現了屬性值修改的通知。由於 KVO 的原理是修改 setter 方法,所以使用 KVO 必須調用 setter 。若直接訪問屬性對象則沒有效果。

重寫class

當修改了isa指向後,class的返回值不會變,但isa的值則發生改變。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface ObjectA: NSObject

@property (nonatomic) NSInteger age;

@end

@implementation ObjectA
@end

@interface ObjectB: NSObject
@end

@implementation ObjectB

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成對象
        ObjectA *objA = [[ObjectA alloc] init];
        ObjectB *objB = [[ObjectB alloc] init];
        
        // 添加Observer以後
        [objA addObserver:objB forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        
        // 輸出ObjectA
        NSLog(@"%@", [objA class]);
        // 輸出NSKVONotifying_ObjectA(object_getClass方法返回isa指向)
        NSLog(@"%@", object_getClass(objA));

    }
    return 0;
}
複製代碼

打印結果: 2018-05-06 22:47:05.538899+0800 KVCKVO[38474:13343992] ObjectA 2018-05-06 22:47:05.539242+0800 KVCKVO[38474:13343992] NSKVONotifying_ObjectA

重寫dealloc

系統重寫 dealloc 方法來釋放資源。

重寫_isKVOA

這個私有方法是用來標示該類是一個 KVO 機制聲稱的類。

如何證實被觀察的類被重寫了以上方法

參考用代碼探討 KVC/KVO 的實現原理這篇文章,經過代碼一步步分析,從斷點截圖來看,能夠很好證實以上被重寫的方法。

關注我

歡迎關注公衆號:jackyshan,技術乾貨首發微信,第一時間推送。

相關文章
相關標籤/搜索