iOS-KVC与KVO

KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。

KVC

KVC有两种读取方式,一种通过key读取,一种通过keypath读取.

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable ObjectType)valueForKey:(NSString *)key;
/* Key-path-taking variants of like-named methods. The default implementation of each parses the key path enough to determine whether or not it has more than one component (key path components are separated by periods). If so, -valueForKey: is invoked with the first key path component as the argument, and the method being invoked is invoked recursively on the result, with the remainder of the key path passed as an argument. If not, the like-named non-key-path-taking method is invoked.
*/
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

简单对比一下两种取值方式的差别,代码如下:

    NSDictionary *dict = @{@"name":@"FlyElephant",
                            @"address":@{
                                    @"provice":@"a1",
                                    @"detail":@{
                                            @"floor":@"10",
                                            }
                                    }
                            };
    
    NSDictionary *address = [dict valueForKey:@"address"];
    NSDictionary *detail = [address valueForKey:@"detail"];
    NSLog(@"%@",detail);
    NSDictionary *detail2 = [dict valueForKeyPath:@"address.detail"];
    NSLog(@"%@",detail2);

取值结果一致,valueForKey只能去最上层的结果,对下层嵌套的数据无法获取。valueForKeyPath可以进行嵌套取值,层次较深,取值较方便。

{
    floor = 10;
}
{
    floor = 10;
}

KVC 寻找key

setValue:forKey:寻找过程

  • 调用 set<Key>: 或者 _set<Key> 设置方法,如果方法存在,结束查找。
  • 检查 + (BOOL)accessInstanceVariablesDirectly 方法是否返回 YES。该方法默认返回 YES,继续查找。如果重写设置为 NO,执行 - (void)setValue:forUndefinedKey: 方法,默认是抛出异常,不推荐设置为 NO。
  • 按照 _<key>,_is<Key>,<key> 和 is<Key> 的顺序在类接口定义和实现处查找实例变量,再赋值。如果上述方法和实例变量都不存在,就执行 - (void)setValue:forUndefinedKey:。

valueForKey:

  • 首先按get<Key> <key> is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者int等值类型, 会做NSNumber转换,如果不存在,继续查找。
  • 如果countOf<Key>和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合,它是NSKeyValueArray,是NSArray的子类,这个代理集合将拥有以上方法的组合,还有一个可选的get<Key>: range:方法;
countOf<Key> & objectIn<Key>AtIndex & <Key>AtIndex

所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

  • 如果上面的方法没有找到,那么会查找:
countOf<Key> & enumeratorOf<Key> & memberOf<Key>

以上三种格式的方法,如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合,这个代理集合将拥有以上三种方法。

  • 如果还没有找到,再检查类方法:
+ (BOOL)accessInstanceVariablesDirectly

如果返回YES(默认行为),那么和先前的设值一样,会按_Key,_isKey,Key,isKey的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱;返回NO,继续执行;

  • 调用valueForUndefinedKey

KVC 的应用

getter / setter

非KVC设值:

    //赋值
    Account *account = [[Account alloc] init];
    account.userName = @"FlyElephant";
    Address *address = [[Address alloc] init];
    address.province = @"北京";
    account.address = address;
    //取值
    NSString *userName =  account.userName;
    NSString *province = account.address.province;
    NSLog(@"%@---%@",userName,province);

KVC 设置值:

    // 赋值
    Account *account = [[Account alloc] init];
    [account setValue:@"FlyElephant" forKey:@"userName"];
    Address *address = [[Address alloc] init];
    [account setValue:address forKey:@"address"];
    [account setValue:@"北京" forKeyPath:@"address.province"];
    // 取值
    NSString *userName =  [account valueForKey:@"userName"];
    NSString *province = [account valueForKeyPath:@"address.province"];
    NSLog(@"%@---%@",userName,province);

JSON转Model

JSON转Model通过runtime获取类的所有属性,然后对属性进行赋值,开源框架YYModel非常值得学习。

                        NSValue *value = [self valueForKey:NSStringFromSelector(propertyMeta->_getter)];
                        if (value) {
                            [one setValue:value forKey:propertyMeta->_name];
                        }

修改系统控件隐藏属性

UIPageControl头文件代码如下:

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIPageControl : UIControl 

@property(nonatomic) NSInteger numberOfPages;          // default is 0
@property(nonatomic) NSInteger currentPage;            // default is 0. value pinned to 0..numberOfPages-1

@property(nonatomic) BOOL hidesForSinglePage;          // hide the the indicator if there is only one page. default is NO

@property(nonatomic) BOOL defersCurrentPageDisplay;    // if set, clicking to a new page won't update the currently displayed page until -updateCurrentPageDisplay is called. default is NO
- (void)updateCurrentPageDisplay;                      // update page display to match the currentPage. ignored if defersCurrentPageDisplay is NO. setting the page value directly will update immediately

- (CGSize)sizeForNumberOfPages:(NSInteger)pageCount;   // returns minimum size required to display dots for given page count. can be used to size control if page count could change

@property(nullable, nonatomic,strong) UIColor *pageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic,strong) UIColor *currentPageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;

@end

获取UIPageControl类的隐藏变量,方法代码如下:

- (NSArray *)getIvarList:(Class)cls {
    NSMutableArray *arr = [NSMutableArray array];
    unsigned int outCount;
    Ivar *ivars = class_copyIvarList(cls, &outCount);
    for (NSInteger i=0; i<outCount; ++i) {
        Ivar ivar = ivars[i];
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        NSString *str = [name stringByAppendingFormat:@"---%@",type];
        [arr addObject:str];
    }
    free(ivars);
    return [arr copy];
}

测试代码:

    UIPageControl *pageControl = [[UIPageControl alloc] init];
    NSArray *array = [self getIvarList:[pageControl class]];
    NSLog(@"%@",array);

测试结果:

  "_lastUserInterfaceIdiom---q",
    "_indicators---@\"NSMutableArray\"",
    "_currentPage---q",
    "_displayedPage---q",
    "_pageControlFlags---{?=\"hideForSinglePage\"b1\"defersCurrentPageDisplay\"b1}",
    "_currentPageImage---@\"UIImage\"",
    "_pageImage---@\"UIImage\"",
    "_currentPageImages---@\"NSMutableArray\"",
    "_pageImages---@\"NSMutableArray\"",
    "_backgroundVisualEffectView---@\"UIVisualEffectView\"",
    "_currentPageIndicatorTintColor---@\"UIColor\"",
    "_pageIndicatorTintColor---@\"UIColor\"",
    "_legibilitySettings---@\"_UILegibilitySettings\"",
    "_numberOfPages---q"

可以利用KVC设置_currentPageImage和_pageImage.

Storyboard

在Storyboard中,也可以使用KVC,设置控件的属性,如图所示:


KVC设置.png
  • 不建议使用,如果团队开发,其他成员经手代码很容易忽略这块的代码,最好都在代码中进行设置;

KVO

KVO(Key Value Observer)键值观察者,是观察者设计模式的一种。KVO的观察者,监测被观察者的某属性是否发生变化,若被监测的属性发生的更改,会触发观察者的一个方法。使用KVO需要注册监听器,也需要删除监听器。

KVO 基本使用

  • 监听和移除方法
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
  • NSKeyValueObservingOptions枚举:
/* Options for use with -addObserver:forKeyPath:options:context: and -addObserver:toObjectsAtIndexes:forKeyPath:options:context:.
*/
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {

    /* Whether the change dictionaries sent in notifications should contain NSKeyValueChangeNewKey and NSKeyValueChangeOldKey entries, respectively.
    */
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,

    /* Whether a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew is also specified but will never contain an NSKeyValueChangeOldKey entry. (In an initial notification the current value of the observed property may be old, but it's new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer's -observeValueForKeyPath:ofObject:change:context: method. When this option is used with -addObserver:toObjectsAtIndexes:forKeyPath:options:context: a notification will be sent for each indexed object to which the observer is being added.
    */
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,

    /* Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is [NSNumber numberWithBool:YES], but never contains an NSKeyValueChangeNewKey entry. You can use this option when the observer's own KVO-compliance requires it to invoke one of the -willChange... methods for one of its own properties, and the value of that property depends on the value of the observed object's property. (In that situation it's too late to easily invoke -willChange... properly in response to receiving an -observeValueForKeyPath:ofObject:change:context: message after the change.)

When this option is specified, the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified, except for ordered unique to-many relationships represented by NSOrderedSets.  For those, for NSKeyValueChangeInsertion and NSKeyValueChangeReplacement changes, the change dictionary for a will-change notification contains an NSKeyValueChangeIndexesKey (and NSKeyValueChangeOldKey in the case of Replacement where the NSKeyValueObservingOptionOld option was specified at registration time) which give the indexes (and objects) which *may* be changed by the operation.  The second notification, after the change, contains entries reporting what did actually change.  For NSKeyValueChangeRemoval changes, removals by index are precise.
    */
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08

};
  • 添加和监听观察者:
    self.account = [[Account alloc] init];
    self.account.userName = @"Fly";
    [self.account addObserver:self forKeyPath:@"userName" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"userName"]) {
        NSLog(@"%@",change);
    }
}
  • 取消对键值的监听
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"userName"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

实现原理

KVO是根据isa-swizzling技术来实现的,主要依据runtime的强大动态能力。当类A第一次被观察时,系统会在运行时期动态的创建一个该类的派生类NSKVONotifying_A。NSKVONotifying_A类中重写任何被观察属性的setter方法。

account监听之后类及方法的改变:

    self.account = [[Account alloc] init];
    self.account.userName = @"Fly";
    NSLog(@"before observer isa:%@---class:%@",object_getClass(self.account), [self.account class]);
    NSArray *originMethod = [self getMethodList:object_getClass(self.account)];
    NSLog(@"class:%@---method:%@",object_getClass(self.account),originMethod);
    [self.account addObserver:self forKeyPath:@"userName" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"after observer isa:%@---class:%@",object_getClass(self.account), [self.account class]);
    NSArray *newMethod = [self getMethodList:object_getClass(self.account)];
    NSLog(@"class:%@---method:%@",object_getClass(self.account),newMethod);
  • 添加观察者之前的类及方法:
before observer isa:Account---class:Account
class:Account---method:(
    "setUserName:---v24@0:8@16",
    "userName---@16@0:8",
    "address---@16@0:8",
    ".cxx_destruct---v16@0:8",
    "password---@16@0:8",
    "setPassword:---v24@0:8@16",
    "setAddress:---v24@0:8@16"
)
  • 添加观察者之后的类及方法:
after observer isa:NSKVONotifying_Account---class:Account
class:NSKVONotifying_Account---method:(
    "setUserName:---v24@0:8@16",
    "class---#16@0:8",
    "dealloc---v16@0:8",
    "_isKVOA---B16@0:8"
)

新增NSKVONotifying_Account的四个方法:

  • setUserName
    会调用
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

然后在didChangeValueForKey 中,去调用observeValueForKeyPath方法:

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

如果没有执行setter之类的调用,那么使用setValue:forKey方法也会直接调用observeValueForKeyPath:keyPath :object :change :context方法。
如果既没有调用setter也没有调用setValue:forKey,那么显示调用:

   [self.account willChangeValueForKey:@"userName"];
   [self.account didChangeValueForKey:@"userName"];

就会触发observeValueForKeyPath:keyPath :object :change :context方法,同样可以使用KVO。

  • class
    当修改了isa指向后,isa的值则发生改变,class返回跟重写继承类之前同样的内容。
  • dealloc
    观察移除后使class的isa指向原来的类,释放资源;
  • _isKVO
    判断被观察者自己是否同时也观察了其他对象

参考链接

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOImplementation.html

https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html

https://lpd-ios.github.io/2017/03/11/KVC-KVO/

https://techbird.me/2018/05/23/ios-kvc-and-kvo/#KVO%E7%9A%84%E5%8E%9F%E7%90%86

http://southpeak.github.io/2015/04/23/cocoa-foundation-nskeyvalueobserving/

https://tianziyao.github.io/2016/02/08/iOS%E6%A8%A1%E5%9E%8B%20-%20KVC/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容

  • KVC(Key-value coding)键值编码,指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性...
    baiwulong阅读 166评论 0 0
  • KVC、KVO概述 KVC(NSKeyValueCoding) "键-值 编码"是一种间接访问对象的属性的机制...
    Joker_King阅读 991评论 1 4
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,097评论 1 32
  • 级别: ★★☆☆☆标签:「iOS」「KVC」「KVO」作者: dac_1033审校: QiShare团队 一、 K...
    QiShare阅读 1,493评论 4 14
  • 最近有些浑浑噩噩,却又好像那么清晰的知道自己要做什么……… 这种感觉神奇的让我想到了———跑步! 一说跑步就自然而...
    玄同子阅读 131评论 0 1