KVC KVO高階應用

JeaEger 8年前發布 | 8K 次閱讀 iOS開發 KVC KVO 移動開發

 

KVC, KVO作為一種魔法貫穿日常Cocoa開發,筆者原先是準備寫一篇對其的全面總結,可網絡上對其的表面介紹已經夠多了,除去基本層面的使用,筆者跟大家談下平常在網絡上沒有提及的KVC, KVO進階知識。旨在分享交流。

 

KVC的消息傳遞

valueForKey: 的使用并不僅僅用來取值那么簡單,還有很多特殊的用法,集合類也覆蓋了這個方法,通過調用 valueForKey: 給容器中每一個對象發送操作消息,并且結果會被保存在一個新的容器中返回,這樣我們能很方便地利用一個容器對象創建另一個容器對象。另外,valueForKeyPath:還能實現多個消息的傳遞。一個例子:

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
NSArray *values = [object valueForKeyPath:@"@unionOfObjects.value"];  

@distinctUnionOfObjects操作符返回被操作對象指定屬性的集合并做去重操作,而@unionOfObjects則允許重復。如果其中任何涉及的對象為nil,則拋出異常。

Array和Set操作符:Array和Set操作符操作對象是嵌套型的集合對象

  • @distinctUnionOfArrays、@unionOfArrays
NSArray *values = [arrayOfobjectsArrays valueForKeyPath:@"@distinctUnionOfArrays.value"];  

同樣的,返回被操作集合下的集合中的對象的指定屬性的集合,并且做去重操作,而@unionOfObjects則允許重復。如果其中任何涉及的對象為nil,則拋出異常。

  • @distinctUnionOfSets
NSSet *values = [setOfobjectsSets valueForKeyPath:@"@distinctUnionOfSets.value"];  

返回結果同理于NSArray。

據官方文檔說明,目前還不支持自動以操作符。

KVC與容器類(集合代理對象)

當然對象的屬性可以是一對一的,也可以是一對多。屬性的一對多關系其實就是一種對容器類的映射。如果有一個名為numbers的數組屬性,我們可以使用 valueForKey:@"numbers" 來獲取,這個是沒問題的,但KVC還能使用更靈活的方式管理集合。——集合代理對象

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上例中我們實現了第二條中的特殊命名函數組合:

- (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。

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(@"remove\n");
}
@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
    (
    )
}

上例中,我們通過調用

- mutableArrayValueForKey:
- mutableSetValueForKey:
- mutableOrderedSetValueForKey:

KVC會給我們返回一個代理可變容器delegateElfins,通過對代理可變容器的操作,KVC會自動調用合適KVC方法(如下):

//至少實現一個insert方法和一個remove方法
- insertObject:inValAtIndex:
- removeObjectFromValAtIndex:
- insertVal:atIndexes:
- removeValAtIndexes:

間接地對被代理對象操作。還有一組更強大的方法供參考

- replaceObjectInValAtIndex:withObject:
- replaceValAtIndexes:withVal:

我認為這就是KVC結合KVO的結果。這里我嘗試研究下了文檔中的如下兩個方法,還沒有什么頭緒,知道的朋友可否告訴我下

- willChange:valuesAtIndexes:forKey:
- didChange:valuesAtIndexes:forKey:

KVO和容器類

要注意,對容器類的觀察與對非容器類的觀察并不一樣,不可變容器的內容發生改變并不會影響他們所在的容器,可變容器的內容改變&內容增刪也都不會影響所在的容器,那么如果我們需要觀察某容器中的對象,首先我們得觀察容器內容的變化,在容器內容增加時添加對新內容的觀察,在內容移除同時移除對該內容的觀察。

既然容器內容數量改變和內容自身改變都不會觸發容器改變,此時對容器屬性施加KVO并沒有效果,那么怎么實現對容器變化(非容器改變)的觀察呢?上面所介紹的代理容器能幫到我們:

//我們通過KVC拿到容器屬性的代理對象
NSMutableArray *delegateElfins = [ElfinsArray mutableArrayValueForKey:@"elfins"];  
[delegateElfins addObject:@"小精靈10"];

當然這樣做的前提是要實現 insertObject:inValAtIndex: 和 removeObjectFromValAtIndex: 兩個方法。如此才能觸發 observeValueForKeyPath:ofObject:change:context: 的響應。

而后,我們就可以輕而易舉地在那兩個方法實現內對容器新成員添加觀察/對容器廢棄成員移除觀察。

KVO的實現原理

寫到這里有點犯困,估計廣州的春天真的來了。對于KVO的實現原理就不花筆墨再描述了,網絡上哪里都能找到,這里借網上一張圖來偷懶帶過。

在我們了解明白實現原理的前提下,我們可以自己來嘗試模仿,那么我們從哪里下手呢?先來準備一個新子類的setter方法:

- (void)notifySetter:(id)newValue {
    NSLog(@"我是新的setter");
}

setter的實現先留空,下面再詳細說,緊接著,我們直接進入主題,runtime注冊一個新類,并且讓被監聽類的isa指針指向我們自己偽造的類,為了大家看得方便,筆者就不做封裝了,所有直接寫在一個方法內:

- (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實現:

- (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跳進去來看看這個結構體:

/// 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代表當前類的父類,也就是這里接受消息目標的類。

工作已經完成了,可以隨便玩了:

- (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

來自: http://zyden.vicp.cc/advanced-kvc-kvo/

 本文由用戶 JeaEger 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!