KVC, KVO 作為一種魔法貫穿日常Cocoa開發,筆者原先是準備寫一篇對其的全面總結,可網路上對其的表面介紹已經夠多了,除去基本層面的使用,筆者跟大家談下平常在網路上沒有提及的KVC, KVO進階知識。旨在分享交流。
KVC的訊息傳遞
valueForKey:
的使用並不僅僅用來取值那麼簡單,還有很多特殊的用法,集合類也覆蓋了這個方法,通過呼叫valueForKey:
給容器中每一個物件傳送操作訊息,並且結果會被儲存在一個新的容器中返回,這樣我們能很方便地利用一個容器物件建立另一個容器物件。另外,valueForKeyPath:還能實現多個訊息的傳遞。一個例子:
1 2 3 4 5 6 7 8 9 10 |
NSArray *array = [NSArray arrayWithObject:@"10.11", @"20.22", nil]; NSArray *resultArray = [array valueForKeyPath:@"doubleValue.intValue"]; NSLog(@"%@", resultArray); //列印結果 ( 10, 20 ) |
KVC容器操作
容器不僅僅能使用KVC方法實現對容器成員傳遞普通的操作訊息,KVC還定義了特殊的一些常用操作,使用valueForKeyPath:
結合操作符
來使用,所定義的keyPath格式入下圖所示 Left key path:如果有,則代表需要操作的物件路徑(相對於呼叫者)
Collection operator:以”@”開頭的操作符
Right key path:指定被操作的屬性
常規操作符:
- @avg、@count、@max、@min、@sum
物件操作符:
- @distinctUnionOfObjects、@unionOfObjects
1 |
NSArray *values = [object valueForKeyPath:@"@unionOfObjects.value"]; |
@distinctUnionOfObjects操作符返回被操作物件指定屬性的集合並做去重操作,而@unionOfObjects則允許重複。如果其中任何涉及的物件為nil,則丟擲異常。
Array和Set操作符:
Array和Set操作符操作物件是巢狀型的集合物件
- @distinctUnionOfArrays、@unionOfArrays
1 |
NSArray *values = [arrayOfobjectsArrays valueForKeyPath:@"@distinctUnionOfArrays.value"]; |
同樣的,返回被操作集合下的集合中的物件的指定屬性的集合,並且做去重操作,而@unionOfObjects則允許重複。如果其中任何涉及的物件為nil,則丟擲異常。
- @distinctUnionOfSets
1 |
NSSet *values = [setOfobjectsSets valueForKeyPath:@"@distinctUnionOfSets.value"]; |
返回結果同理於NSArray。
據官方文件說明,目前還不支援自動以操作符。
KVC與容器類(集合代理物件)
當然物件的屬性可以是一對一的,也可以是一對多。屬性的一對多關係其實就是一種對容器類的對映。如果有一個名為numbers的陣列屬性,我們可以使用valueForKey:@"numbers"
來獲取,這個是沒問題的,但KVC還能使用更靈活的方式管理集合。——集合代理物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
ElfinsArray.h @interface ElfinsArray : NSObject @property (assign ,nonatomic) NSUInteger count; - (NSUInteger)countOfElfins; - (id)objectInElfinsAtIndex:(NSUInteger)index; @end ElfinsArray.m #import "ElfinsArray.h" @implementation ElfinsArray - (NSUInteger)countOfElfins { return self.count; } - (id)objectInElfinsAtIndex:(NSUInteger)index { return [NSString stringWithFormat:@"小精靈%lu", (unsigned long)index]; } @end Main.m - (void)work { ElfinsArray *elfinsArr = [ElfinsArray alloc] init]; elfinsArr.count = 3; NSArray *elfins = [ElfinsArray valueForKey:@"elfins"]; //elfins為KVC代理陣列 NSLog(@"%@", elfins); //列印結果 ( "小精靈0", "小精靈1", "小精靈2" ) } |
問題來了,ElfinsArray中並沒有定義elfins屬性,那麼elfins陣列從何而來?valueForKey:
有如下的搜尋規則:
- 按順序搜尋getVal、val、isVal,第一個被找到的會用作返回。
- countOfVal,或者objectInValAtIndex:與valAtIndexes其中之一,這個組合會使KVC返回一個代理陣列。
- countOfVal、enumeratorOfVal、memberOfVal。這個組合會使KVC返回一個代理集合。
- 名為val、isVal、val、isVal的例項變數。到這一步時,KVC會直接訪問例項變數,而這種訪問操作破壞了封裝性,我們應該儘量避免,這可以通過重寫+accessInstanceVariablesDirectly返回NO來避免這種行為。
ok上例中我們實現了第二條中的特殊命名函式組合:
1 2 |
- (NSUInteger)countOfElfins; - (id)objectInElfinsAtIndex:(NSUInteger)index; |
這使得我們呼叫valueForKey:@"elfins"
時,KVC會為我們返回一個可以響應NSArray所有方法的代理陣列物件(NSKeyValueArray),這是NSArray的子類,- (NSUInteger)countOfElfins
決定了這個代理陣列的容量,- (id)objectInElfinsAtIndex:(NSUInteger)index
決定了代理陣列的內容。本例中使用的key是elfins,同理的如果key叫human,KVC就會去尋找-countOfHuman:
可變容器呢
當然我們也可以在可變集合(NSMutableArray、NSMutableSet、NSMutableOrderedSet)中使用集合代理:
這個例子我們不再使用KVC給我們生成代理陣列,因為我們是通過KVC拿到的,而不能主動去操作它(insert/remove),我們宣告一個可變陣列屬性elfins。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
ElfinsArray.h @interface ElfinsArray : NSObject @property (strong ,nonatomic) NSMutableArray *elfins; - (void)insertObject:(id)object inNumbersAtIndex:(NSUInteger)index; - (void)removeObjectFromNumbersAtIndex:(NSUInteger)index; @end ElfinsArray.m #import "ElfinsArray.h" @implementation ElfinsArray - (void)insertObject:(id)object inElfinsAtIndex:(NSUInteger)index { [self.elfins insertObject:object atIndex:index]; NSLog(@"insert %@n", object); } - (void)removeObjectFromElfinsAtIndex:(NSUInteger)index { [self.elfins removeObjectAtIndex:index]; NSLog(@"removen"); } @end Main.m - (void)work { ElfinsArray *elfinsArr = [ElfinsArray alloc] init]; elfinsArr.elfins = [NSMutableArray array]; NSMutableArray *delegateElfins = [ElfinsArray mutableArrayValueForKey:@"elfins"]; //delegateElfins為KVC代理可變陣列,非指向elfinsArr.elfins [delegateElfins insertObject:@"小精靈10" atIndex:0]; NSLog(@"first log n %@", elfinsArr.elfins); [delegateElfins removeObjectAtIndex:0]; NSLog(@"second log n %@", elfinsArr.elfins); //列印結果 insert 小精靈10 first log ( "小精靈10" ) remove second log ( ) } |
上例中,我們通過呼叫
1 2 3 |
- mutableArrayValueForKey: - mutableSetValueForKey: - mutableOrderedSetValueForKey: |
KVC會給我們返回一個代理可變容器delegateElfins,通過對代理可變容器的操作,KVC會自動呼叫合適KVC方法(如下):
1 2 3 4 5 |
//至少實現一個insert方法和一個remove方法 - insertObject:inValAtIndex: - removeObjectFromValAtIndex: - insertVal:atIndexes: - removeValAtIndexes: |
間接地對被代理物件操作。
還有一組更強大的方法供參考
1 2 |
- replaceObjectInValAtIndex:withObject: - replaceValAtIndexes:withVal: |
我認為這就是KVC結合KVO的結果。這裡我嘗試研究下了文件中的如下兩個方法,還沒有什麼頭緒,知道的朋友可否告訴我下
1 2 |
- willChange:valuesAtIndexes:forKey: - didChange:valuesAtIndexes:forKey: |
KVO和容器類
要注意,對容器類的觀察與對非容器類的觀察並不一樣,不可變容器的內容發生改變並不會影響他們所在的容器,可變容器的內容改變&內容增刪也都不會影響所在的容器,那麼如果我們需要觀察某容器中的物件,首先我們得觀察容器內容的變化,在容器內容增加時新增對新內容的觀察,在內容移除同時移除對該內容的觀察。
既然容器內容數量改變和內容自身改變都不會觸發容器改變,此時對容器屬性施加KVO並沒有效果,那麼怎麼實現對容器變化(非容器改變)的觀察呢?上面所介紹的代理容器能幫到我們:
1 2 3 |
//我們通過KVC拿到容器屬性的代理物件 NSMutableArray *delegateElfins = [ElfinsArray mutableArrayValueForKey:@"elfins"]; [delegateElfins addObject:@"小精靈10"]; |
當然這樣做的前提是要實現insertObject:inValAtIndex:
和removeObjectFromValAtIndex:
兩個方法。如此才能觸發observeValueForKeyPath:ofObject:change:context:
的響應。
而後,我們就可以輕而易舉地在那兩個方法實現內對容器新成員新增觀察/對容器廢棄成員移除觀察。
KVO的實現原理
寫到這裡有點犯困,估計廣州的春天真的來了。對於KVO的實現原理就不花筆墨再描述了,網路上哪裡都能找到,這裡借網上一張圖來偷懶帶過。
在我們瞭解明白實現原理的前提下,我們可以自己來嘗試模仿,那麼我們從哪裡下手呢?先來準備一個新子類的setter方法:
1 2 3 |
- (void)notifySetter:(id)newValue { NSLog(@"我是新的setter"); } |
setter的實現先留空,下面再詳細說,緊接著,我們直接進入主題,runtime註冊一個新類,並且讓被監聽類的isa指標指向我們自己偽造的類,為了大家看得方便,筆者就不做封裝了,所有直接寫在一個方法內:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (Class)configureKVOSubClassWithSourceClassName:(NSString *)className observeProperty:(NSString *)property { NSString *prefix = @"NSKVONotifying_"; NSString *subClassName = [prefix stringByAppendingString:className]; //1 Class originClass = [KVOTargetClass class]; Class dynaClass = objc_allocateClassPair(originClass, subClassName.UTF8String, 0); //重寫property對應setter NSString *propertySetterString = [@"set" stringByAppendingString:[[property substringToIndex:1] uppercaseString]]; propertySetterString = [propertySetterString stringByAppendingString:[property substringFromIndex:1]]; propertySetterString = [propertySetterString stringByAppendingString:@":"]; SEL setterSEL = NSSelectorFromString(propertySetterString); //2 Method setterMethod = class_getInstanceMethod(originClass, setterSEL); const char types = method_getTypeEncoding(setterMethod); class_addMethod(dynaClass, setterSEL, class_getMethodImplementation([self class], @selector(notifySetter:)), types); objc_registerClassPair(dynaClass); return dynaClass; } |
我們來看
//1處,我們要建立一個新的類,可以通過objc_allocateClassPair
來建立這個新類和他的元類,第一個引數需提供superClass的類物件,第二個引數接受新類的類名,型別為const char *
,通過返回值我們得到dynaClass類物件。
//2處,我們希望為我們的偽造的類新增跟被觀察類一樣只能的setter方法,我們可以藉助被觀察類,拿到型別編碼資訊,通過class_addMethod
,注入我們自己的setter方法實現:class_getMethodImplementation([self class], @selector(notifySetter:))
,最後通過objc_registerClassPair
完成新類的註冊!。
可能有朋友會問class_getMethodImplementation
中獲取IMP的來源[self class]
的self是指代什麼?其實就是指代我們自己的setter(notifySetter:)IMP實現所在的類,指代從哪個類可以找到這個IMP,筆者這裡是直接開一個新工程,在ViewController裡就開乾的,notifySetter:
和這個手術方法configureKVOSubClassWithSourceClassName: observeProperty:
所在的地方就是VC,因此self指向的就是這個VC例項,也就是這個手術方法的呼叫者。
不用懷疑,經過手術後對KVOTargetClass對應屬性的修改,就會進入到我們偽裝的setter,下面我們來完成先前留空的setter實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)notifySetter:(id)newValue { NSLog(@"我是新的setter"); struct objc_super originClass = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; NSString *setterName = NSStringFromSelector(_cmd); NSString *propertyName = [setterName substringFromIndex:3]; propertyName = [[propertyName substringToIndex:propertyName.length - 1] lowercaseString]; [self willChangeValueForKey:propertyName]; //呼叫super的setter //1 void (*objc_msgSendSuperKVO)(void * class, SEL _cmd, id value) = (void *)objc_msgSendSuper; //2 objc_msgSendSuperKVO(&originClass, _cmd, newValue); [self didChangeValueForKey:propertyName]; } |
我們輕而易舉地讓willChangeValueForKey:
和didChangeValueForKey:
包裹了對newValue的修改。
這裡需要提的是:
//1處,在IOS8後,我們不能直接使用objc_msgSend()
或者objc_msgSendSuper()
來傳送訊息,我們必須自定義一個msg_Send函式並提供具體型別來使用。
//2處,至於objc_msgSendSuper(struct objc_super *, SEL, ...)
,第一個引數我們需要提供一個objc_super結構體,我們command跳進去來看看這個結構體:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained id receiver; /// Specifies the particular superclass of the instance to message. #if !defined(__cplusplus) & !__OBJC2__ /* For compatibility with old objc-runtime.h header */ __unsafe_unretained Class class; #else __unsafe_unretained Class super_class; #endif /* super_class is the first class to search */ }; #endif |
第一個成員receiver表示某個類的例項,第二個成員super_class代表當前類的父類,也就是這裡接受訊息目標的類。
工作已經完成了,可以隨便玩了:
1 2 3 4 5 6 7 8 9 |
- (void)main { KVOTargetClass *kvoObject = [[KVOTargetClass alloc] init]; NSString *targetClassName = NSStringFromClass([KVOTargetClass class]); Class subClass = [self configureKVOSubClassWithSourceClassName:targetClassName observeProperty:@"name"]; object_setClass(kvoObject, subClass); [kvoObject setName:@"haha"]; NSLog(@"property -- %@", kvoObject.name); } |
KVO驗證筆者就懶得驗了,有興趣的朋友可以試試。最後,感謝!
參考文獻
objc.io
NSKeyValueObserving Protocol Reference
Apple developer
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式