[iOS] KVC KVO

KCV

  • 其實由于ObjC的語言特性,你根部不必進行任何操作就可以進行屬性的動態讀寫,這種方式就是Key Value Coding(簡稱KVC)。
  • 鍵值對編碼意思是,能夠通過數據成員的名字來訪問到它的值
  • KVC的操作方法由NSKeyValueCoding協議提供,而NSObject就實現了這個協議,也就是說ObjC中幾乎所有的對象都支持KVC操作,常用的KVC操作方法如下:
    • 動態設置: setValue:屬性值 forKey:屬性名(用于簡單路徑)、**setValue:屬性值 forKeyPath:屬性路徑
      **(用于復合路徑,例如Person有一個Account類型的屬性,那么person.account就是一個復合屬性)
    • 動態讀取: **valueForKey:屬性名
      valueForKeyPath:屬性名
      **(用于復合路徑)

KVC的使用

KVC 動態設置

  • 單層設值(簡單路徑)
    • - (void)setValue:(nullable id)value forKey:(NSString *)key;
    People *bPel = [People new];
    // 單層設值
    [bPel setValue:@"譚譚譚" forKey:@"name"];
    [bPel setValue:@(20) forKey:@"age"];
  • 多層設置(復合路徑xxx.xxx)
    • - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    People *bPel = [People new];
    bPel.dog = [[Dog alloc] init];
    [bPel setValue:@(20) forKeyPath:@"age"];
    [bPel setValue:@"啊啊啊" forKeyPath:@"dog.name"];
    [bPel setValue:@(300) forKeyPath:@"dog.price"];
  • 私有成員設值
    @implementation People
    {
        double _hight;
        @private int _score;
    }
    // 創建people對象
    People *bbPel = [People new];
    // 單層設值
    [bbPel setValue:@"我我我" forKey:@"_name"];
    // 私有成員設置
    [bbPel setValue:@"170.0" forKey:@"_hight"];
    [bbPel setValue:@"99" forKey:@"_score"];
    // 類的.h文件中寫的一個方法用來看是否設置了私有成員,打印操作
    [bbPel print];
  • 字典轉模型
    • 使用- (void)setValuesForKeysWithDictionary:方法進行字典轉模型
    • 前提:字典中的key必須和模型中的屬性一模一樣(個數 + 名稱)
    • 注意:只能對當前調用KVC方法的對象進行轉換,不能對它的屬性對象進行轉換
    • 原理:其實就是根據字典中的key和對應的value,一個個地,通過setValeu:forKeyPath方法進行賦值,從而轉換成模型
#import <Foundation/Foundation.h>
@class Dog;
@interface PeopleModel : NSObject
/** 名字*/
@property (nonatomic, copy) NSString *name;
/** 年齡*/
@property (nonatomic, assign) float height;
/** 狗*/
@property (nonatomic, strong) Dog *dog;
@end
 
- (void)viewDidLoad {
    [super viewDidLoad];
    // 創建字典
    NSDictionary *dict = @{
                           @"name":@"Sam",
                           @"height":@(1.75),
                           @"dog": @{
                                   @"name":@"WangCai"
                                   }
                           };
    PeopleModel *model = [PeopleModel new];
    // 字典轉模型
    [model setValuesForKeysWithDictionary:dict];
    NSLog(@"%@", model);
    // 需要注意的是,model里面的dog應該是字典類型而不是dog類型
    // 因為setValuesForKeysWithDictionary:方法不能對對象屬性進行轉換
}

KVC 動態讀取

  • 單層讀取
    • - (id)valueForKey:(NSString *)key;
NSString *aName = [aPel valueForKey:@"name"];
NSLog(@"%@",aName);
  • 多層讀取
    • - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
NSString *dogName = [aPel valueForKeyPath:@"dog.name"];
NSLog(@"%@",dogName);
  • 私有變量獲取
int age = [aPel valueForKey:@"_age"];
  • 模型轉字典
    • - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
NSDictionary *dicModel = [aPel dictionaryWithValuesForKeys:@[@"name",@"age"]];
NSLog(@"%@",dicModel);
  • 獲取數組中對象的值
    • 單層的用valueForKey:,多層的用valueForKeyPath:
    People *ppA = [People new];
    ppA.name = @"aaa";
    ppA.age = 1;
    ppA.dog = [Dog new];
    [ppA setValue:@"wangcaiA" forKeyPath:@"dog.name"];
    
    People *ppB = [People new];
    ppB.name = @"bbb";
    ppB.age = 2;
    ppB.dog = [Dog new];
    [ppB setValue:@"wangcaiB" forKeyPath:@"dog.name"];
    
    People *ppC = [People new];
    ppC.name = @"ccc";
    ppC.age = 3;
    ppC.dog = [Dog new];
    [ppC setValue:@"wangcaiC" forKeyPath:@"dog.name"];
    
    People *ppD = [People new];
    ppD.name = @"ddd";
    ppD.age = 4;
    ppD.dog = [Dog new];
    [ppD setValue:@"wangcaiD" forKeyPath:@"dog.name"];
    
    NSArray *arr = @[ppA,ppB,ppC,ppD];
    
    NSArray *nameArr = [arr valueForKeyPath:@"dog.name"];
    NSLog(@"%@", nameArr);
  • 運算符功能
    • @max,@min,@avg等運算符功能
    People *ppA = [People new];
    ppA.name = @"aaa";
    ppA.age = 1;
    ppA.dog = [Dog new];
    [ppA setValue:@"wangcaiA" forKeyPath:@"dog.name"];
    
    People *ppB = [People new];
    ppB.name = @"bbb";
    ppB.age = 2;
    ppB.dog = [Dog new];
    [ppB setValue:@"wangcaiB" forKeyPath:@"dog.name"];
    
    People *ppC = [People new];
    ppC.name = @"ccc";
    ppC.age = 3;
    ppC.dog = [Dog new];
    [ppC setValue:@"wangcaiC" forKeyPath:@"dog.name"];
    
    People *ppD = [People new];
    ppD.name = @"ddd";
    ppD.age = 4;
    ppD.dog = [Dog new];
    [ppD setValue:@"wangcaiD" forKeyPath:@"dog.name"];
    
    NSArray *arr = @[ppA,ppB,ppC,ppD];
    
    NSString *maxName = [arr valueForKeyPath:@"@max.dog.name"];
    NSLog(@"%@", maxName);
    
    int maxAge = [[arr valueForKeyPath:@"@max.age"] intValue];
    NSLog(@"%d", maxAge);

    // 輸出結果
    wangcaiD
    4

KVC的原理

KVC使用起來比較簡單,但是它如何查找一個屬性進行讀取呢?具體查找規則(假設現在要利用KVC對a進行讀取):

  • 如果是動態設置屬性,
    • 1.則優先考慮調用setA方法
    • 2.如果沒有該方法則優先考慮搜索成員變量_a
    • 3.如果仍然不存在則搜索成員變量a
    • 4.如果最后仍然沒搜索到,則會調用這個類的setValue:forUndefinedKey:方法(注意搜索過程中不管這些方法、成員變量是私有的還是公共的都能正確設置);
  • 如果是動態讀取屬性,
    • 1.則優先考慮調用a方法(屬性a的getter方法),
    • 2.如果沒有搜索到則會優先搜索成員變量_a,
    • 3.如果仍然不存在則搜索成員變量a,
    • 4.如果最后仍然沒搜索到,則會調用這個類的valueforUndefinedKey:方法(注意搜索過程中不管這些方法、成員變量是私有的還是公共的都能正確讀取);

KVC的使用場景

  • 場景 1
    • 有些控件的屬性是私有的(比如readonly),但是你又想設置賦值的話
    • 這時候就可以用KVC一般用來設置一些objtevie-c內部的一些不能調用(setOnly)和設置(readOnly)的屬性
  • 場景 2
  • 字典轉模型

KVO

  • 在ObjC中原生支持一種雙向綁定機制,如果數據模型修改了之后會立即反映到UI視圖上,它叫做Key Value Observing(簡稱KVO)。
  • KVO其實是一種觀察者模式,利用它可以很容易實現視圖組件和數據模型的分離,當數據模型的屬性值改變之后作為監聽器的視圖組件就會被激發,激發時就會回調監聽器自身
  • 在ObjC中要實現KVO則必須實現NSKeyValueObServing協議,不過幸運的是NSObject已經實現了該協議,因此幾乎所有的ObjC對象都可以使用KVO。

在ObjC中使用KVO操作常用的方法

  • 注冊指定Key路徑監聽器
  - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • observer :哪個對象(一般是view)監聽
  • keyPath :監聽對象(一般是model)的哪個屬性
  • options :監聽到對象發送改變后,需要傳遞什么值
  • context :注冊監聽時需要傳遞的數據(一般用于監聽多個控制器的時候進行區分)
  • 刪除指定Key路徑的監聽器
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
  • observer:哪個對象監聽
  • keyPath:監聽對象的屬性
  • context:注冊監聽時需要傳遞的數據(一般用于監聽多個控制器的時候進行區分)
  • 回調監聽:
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
  • keyPath:被監聽的對象的屬性
  • object:被監聽的對象
  • change:
  • context:注冊監聽時需要傳遞的數據(一般用于監聽多個控制器的時候進行區分)

KVO的使用步驟

  1. 通過addObserver: forKeyPath: options: context:為被監聽對象(它通常是數據模型)注冊監聽器
  2. 重寫監聽器的observeValueForKeyPath: ofObject: change: context:方法
  3. 刪除監聽器

KVO的注意事項

  • 1.如果使用KVO監聽某個對象的屬性,當對象釋放之前一定要移除監聽
    • 不移除監聽的話,會報錯:
      • 比如 reason: 'An instance 0x7f9483516610 of class Person was deallocated while key value observers were still registered with it.
  • 2.KVO只能監聽通過set方法修改的值
// KVO只能監聽到setter方法
[p setAge:998];
p.age = 998;
//這不是setter方法,KVO監聽不到
p->_age = 998;

KVO實例

假設當商品(模型)的price價格發送改變的時候,我們商品的ShopView展示頁面(視圖)可以及時作出響應。那么此時ShopItemModel就作為我們的被監聽對象,需要ShopVIew為它注冊監聽,而商品展示頁面ShopVIew作為監聽器需要重寫它的observeValueForKeyPath: ofObject: change: context:方法,當監聽的余額發生改變后會回調監聽器ShopVIew監聽方法(observeValueForKeyPath: ofObject: change: context:)。下面通過代碼模擬上面的過程:

ShopItemModel.h

#import <Foundation/Foundation.h>

@interface ShopItemModel : NSObject

/** 商品名稱*/
@property (nonatomic, copy) NSString *name;
/** 價格*/
@property (nonatomic, assign) float price;

@end

ShopItemModel.h

#import "ShopItemModel.h"

@implementation ShopItemModel

@end

ShopView.h

#import <UIKit/UIKit.h>
@class ShopItemModel;
@interface ShopView : UIView

/** 數據模型*/
@property (nonatomic, strong) ShopItemModel *item;

@end

ShopView.m

#import "ShopView.h"
#import "ShopItemModel.h"

@interface ShopView ()

/** 標簽*/
@property (nonatomic, weak) UILabel *tLabel;

@end

@implementation ShopView

-(instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor lightGrayColor];
        // 添加一個標簽
        UILabel *label = [[UILabel alloc] init];
        label.textAlignment = NSTextAlignmentCenter;
        label.text = @"價格";
        [self addSubview:label];
        self.tLabel = label;
    }
    return self;
}

-(void)layoutSubviews {
    [super layoutSubviews];
    
    CGSize size = self.frame.size;
    CGFloat labHeight = size.height * 0.5;
    CGFloat labY = (size.height - labHeight) * 0.5;
    self.tLabel.frame = CGRectMake(0, labY, size.width, labHeight);
}

#pragma mark 設置商品模型
-(void)setItem:(ShopItemModel *)item {
    _item = item;
    
    self.tLabel.text = [NSString stringWithFormat:@"%@價格為:%f", self.item.name, self.item.price];
    
    // 注冊監聽
    [self.item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"商品界面"];
}

#pragma mark 重寫observeValueForKeyPath方法,當商品價格變化后此處獲得通知
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"price"]) {
        NSLog(@"keyPath = %@, object = %@, change = %@, context = %@", keyPath, object, change, context);
        self.tLabel.backgroundColor = [UIColor redColor];
        self.tLabel.text = [NSString stringWithFormat:@"%@價格為:%f", self.item.name, self.item.price];
        NSLog(@"%@知道價格發生變化了,價格為%f", self, self.item.price);
    }
}
#pragma mark 重寫銷毀方法
-(void)dealloc {
    // 移除監聽
    [self.item removeObserver:self forKeyPath:@"price"];
}

ViewController.m

#import "ViewController.h"

#import "ShopView.h"
#import "ShopItemModel.h"

@interface ViewController ()

/** 商品view*/
@property (nonatomic, weak) ShopView *shopView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 創建商品模型
    ShopItemModel *item = [[ShopItemModel alloc] init];
    item.name = @"包包";
    item.price = 20000;

    // 創建商品視圖
    ShopView *sV = [[ShopView alloc] init];
    sV.frame = CGRectMake(0, 200, self.view.frame.size.width, 200);
    // 設置商品視圖的模型
    sV.item = item;
    // 添加到界面
    [self.view addSubview:sV];
    self.shopView = sV;
}

#pragma mark 重寫觸摸事件方法
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.shopView.item.price = 500;  //注意!執行到這一步會觸發監聽器的回調函數observeValueForKeyPath: ofObject: change: context:
}

// 結果:
keyPath = price, object = <ShopItemModel: 0x7f9119d2fca0>, change = {
    kind = 1;
    new = 500;
    old = 20000;
}, context = 商品界面
<ShopView: 0x7f84b1e141f0; frame = (0 200; 414 200); layer = <CALayer: 0x7f84b1e16850>>知道價格發生變化了,價格為500.000000
觸摸前
觸摸后

KVO原理

只要給一個對象注冊一個監聽,那么在運行時:

  1. 系統給這個對象生成一個子類對象 NSKVONotifying_XXX(XXX類名)
  2. 對子類對象中的被監聽屬性重寫setter方法,
  3. 在setter方法中通知監聽者

修改上面例子的setItem方法

-(void)setItem:(ShopItemModel *)item {
    _item = item;
    
    self.tLabel.text = [NSString stringWithFormat:@"%@價格為:%f", self.item.name, self.item.price];
    
    // 打印被監聽對象的isa
    NSLog(@"%@", [self.item valueForKey:@"isa"]);

    // 注冊監聽
    NSKeyValueObservingOptions option =  NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.item addObserver:self forKeyPath:@"price" options:option context:@"商品界面"];

    // 在這里打斷點

    // 打印被監聽對象的isa
    NSLog(@"%@", [self.item valueForKey:@"isa"]);
}

// 輸出結果
ShopItemModel
NSKVONotifying_ShopItemModel
  • 這段代碼說明,當一個對象被監聽之后,會生成一個叫做NSKVONotifying_xxxx的子類對象,并將地址賦給原對象。
    • 簡單地說,當一個對象被監聽之后,會變成一個子類對象
    • 在子類對象中重寫setter方法,里面添加通知,使得當外界調用setter方法后,會發送通知

運行時創建子類,相當于以下代碼操作

#import <Foundation/Foundation.h>

@interface NSKVONotifying_ShopItemModel : ShopItemModel

@end

#import "NSKVONotifying_ShopItemModel.h"

@implementation NSKVONotifying_ShopItemModel

// 重寫了price的setter方法
-(void)setPrice:(float)price {
    // Invoked to inform the receiver that the value of a given property is about to change.
    [self willChangeValueForKey:@"price"];
    _price = price;
    // Invoked to inform the receiver that the value of a given property has changed.
    [self didChangeValueForKey:@"price"];
}

@end

注意:如果你自己定義了這樣一個NSKVONotifing_xxx子類的話,會報錯,因為你自己搞了一個子類,系統又需要搞一個同名子類,就搞不了,所以出錯

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容