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閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容

  • KVC(Key-value coding)鍵值編碼,指iOS的開發中,可以允許開發者通過Key名直接訪問對象的屬性...
    baiwulong閱讀 174評論 0 0
  • KVC、KVO概述 KVC(NSKeyValueCoding) "鍵-值 編碼"是一種間接訪問對象的屬性的機制...
    Joker_King閱讀 1,008評論 1 4
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,120評論 1 32
  • 級別: ★★☆☆☆標簽:「iOS」「KVC」「KVO」作者: dac_1033審校: QiShare團隊 一、 K...
    QiShare閱讀 1,500評論 4 14
  • 最近有些渾渾噩噩,卻又好像那么清晰的知道自己要做什么……… 這種感覺神奇的讓我想到了———跑步! 一說跑步就自然而...
    玄同子閱讀 136評論 0 1