一、KVC 的用法和實踐
用法
KVC(Key-value coding)鍵值編碼,顧名思義。額,簡單來說,是可以通過對象屬性名稱(Key)直接給屬性值(value)編碼(coding)“編碼”可以理解為“賦值”
。這樣可以免去我們調用getter和setter方法,從而簡化我們的代碼,也可以用來修改系統控件內部屬性,KVC是KVO、Core Data、CocoaBindings的技術基礎,他們都是利用了OC的動態性
KVC用法
- setValue:forKey:(為對象的屬性賦值)
- setValue: forKeyPath:(為對象的屬性賦值(包含了setValue:forKey:的功能,並且還可以對對象內的類的屬性進行賦值))
- valueForKey:(根據key取值)
- valueForKeyPath:(根據keyPath取值)
- setValuesForKeysWithDictionary:(對模型進行一次性賦值)
為什么可以用NSNumber來接收int、float的數據類型?
因為:使用valueForKey:時,KVC會自動將標量值(int、float、struct等)翻入NSNumber或NSValue中包裝成一個對象,然后返回。因此,KVC有自動包裝功能。
例如:生成一個這樣子的對象Person
person.h
@class Car; @interface Person : NSObject @property (nonatomic,copy) NSString *name; @property (nonatomic,strong)Car *car; @end
Car.h
@interface Car : NSObject @property (nonatomic,strong) NSNumber *price; @end
在ViewController.m中調用
ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; Person *person=[[Person alloc]init]; [person setValue:@"lxh" forKey:@"name"]; float price=100.0; Car *car=[[Car alloc]init]; person.car=car; [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"]; NSLog(@"%@",person.name); NSLog(@"%f",car.price.floatValue); }
注意點:
- 在Person中我僅僅只是聲明了@class Car,而沒有引用#import "Car.h",然后在ViewController.m中便可以對其進行: [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];這樣子的賦值。所以說明KVC會去自動查找Car類進行賦值
- - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;你會發現value的值必須是id,也就是說不能傳基本數據類型,必須是指針類型的變量。
key和keyPath的區別
keyPath方法是集成了key的所有功能,也就是說對一個對象的一般屬性進行賦值、取值,兩個方法是通用的,都可以實現。但是對對象中的對象進的屬性行賦值,只有keyPath能夠實現。
setValuesForKeysWithDictionary:的巧妙使用(字典轉模型)
-(instancetype)initWithDict:(NSDictionary *)dict{ if (self = [super init]) { [self setValuesForKeysWithDictionary:dict]; } return self; }
注意點:
- 字典轉模型的時候,字典中的某一個key一定要在模型中有對應的屬性
- 如果一個模型中包含了另外的模型對象,是不能直接轉化成功的。
- 通過kvc轉化模型中的模型,也是不能直接轉化成功的
- 底層還是調用了setValue: forKey:
使用例子
(1)修改系統控件內部屬性(runtime + KVC)
例如,界面設計圖是這樣的
怎么感覺有點不同,這UIPageControl
怎么跟我平常用的不一樣?平常不都是這樣的??如下圖
首先想到的肯定是,查看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
不夠用啊兄弟。能不能給我個可以賦值UIImage
對象的屬性?看來正常途徑使用系統的控件是設不了了!如何解呢 ?
第一種方式:自定義UIPageControl 第二種方式:通過runtime遍歷出UIPageControl
所有屬性(包括私有成員屬性,runtime確實很強大)
直接用第二種吧 第一種有興趣的可以自己試試!
使用runtime遍歷UIPageControl
結果如下打印:
2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _lastUserInterfaceIdiom = q 2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _indicators = @"NSMutableArray" 2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _currentPage = q 2016-03-23 01:09:26.161 TenMinDemo[6224:507269] UIPageControl -> _displayedPage = q 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _pageControlFlags = {?="hideForSinglePage"b1"defersCurrentPageDisplay"b1} 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _currentPageImage = @"UIImage" // 當前選中圖片 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _pageImage = @"UIImage" // 默認圖片 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _currentPageImages = @"NSMutableArray" 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _pageImages = @"NSMutableArray" 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _backgroundVisualEffectView = @"UIVisualEffectView" 2016-03-23 01:09:26.162 TenMinDemo[6224:507269] UIPageControl -> _currentPageIndicatorTintColor = @"UIColor" 2016-03-23 01:09:26.163 TenMinDemo[6224:507269] UIPageControl -> _pageIndicatorTintColor = @"UIColor" 2016-03-23 01:09:26.163 TenMinDemo[6224:507269] UIPageControl -> _legibilitySettings = @"_UILegibilitySettings" 2016-03-23 01:09:26.163 TenMinDemo[6224:507269] UIPageControl -> _numberOfPages = q
結果非常滿意,果然找到我想要的圖片設置屬性
然后通過KVC設置自定義圖片,實現了效果,代碼如下
UIPageControl *pageControl = [[UIPageControl alloc] init]; [pageControl setValue:[UIImage imageNamed:@"home_slipt_nor"] forKeyPath:@"_pageImage"]; [pageControl setValue:[UIImage imageNamed:@"home_slipt_pre"] forKeyPath:@"_currentPageImage"];
(2) 在xib/Storyboard中,也可以使用KVC,下面是在xib中使用KVC把圖片邊框設置成圓角
(3)id
{ "id" : "tripleCC", "age" : "30", "address" : "杭州", "schooll" : "HDU" ... }
其中的id是什么?是Objective-C關鍵字,也就是說我定義以下屬性會出現警告:
@property (nonatomic, strong) NSString *id;
雖然可以使用以下方法,對模型中的成員變量進行統一設置,但是出現警告總歸是不好的:
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
底層會調用 setValue:forKey:
既然這樣,可以選擇手動一個個去實現。但是這樣在數據少的時候可以試試,在數據比較多時就不太現實了,程序的可擴展性也不好。
兩種解決方法:
方式1.重寫setValue:forKey:
setValuesForKeysWithDictionary:的底層是調用setValue:forKey:的,所以可以考慮重寫這個方法,並且判斷其key是id時,手動轉換成模型的成員變量名,這里假設把id對應成以下屬性:
@property (nonatomic, strong) NSString *ID;
有了對應的屬性名后,就可以重寫底層方法了
- (void)setValue:(id)value forKey:(NSString *)key { if ([key isEqualToString:@"id"]) { [self setValue:value forKeyPath:@"ID"]; }else{ [super setValue:value forKey:key]; } }
這樣,當使用setValuesForKeysWithDictionary:就不會出現模型中找不到對應的成員變量的錯誤了。
方式2.使用runtime
由於需要針對所有模型使用,可以將其設置為NSObject分類
// dict -> 資源文件提供的字典 // mapDict -> 提供的key映射(實際變量名:資源文件key) + (instancetype)objcWithDict:(NSDictionary *)dict mapDict:(NSDictionary *)mapDict { id objc = [[self alloc] init]; // 遍歷模型中成員變量 unsigned int outCount = 0; Ivar *ivars = class_copyIvarList(self, &outCount); for (int i = 0 ; i < count; i++) { Ivar ivar = ivars[i]; // 成員變量名稱 NSString *ivarName = @(ivar_getName(ivar)); // 獲取出來的是`_`開頭的成員變量名,需要截取`_`之后的字符串 ivarName = [ivarName substringFromIndex:1]; id value = dict[ivarName]; // 由外界通知內部,模型中成員變量名對應字典里面的哪個key // ID -> id if (value == nil) { if (mapDict) { NSString *keyName = mapDict[ivarName]; value = dict[keyName]; } } [objc setValue:value forKeyPath:ivarName]; } return objc; }
使用方法:
+ (instancetype)itemWithDict:(NSDictionary *)dict { // 傳入key和實例變量名的映射字典@{@"ID":@"id"} TPCItem *item = [TPCItem objcWithDict:dict mapDict:@{@"ID":@"id"}]; return item; }
二、底層原理的分析
KVC的賦值原理
setValue:forKey:賦值原理如下:
- 去模型中查找有沒有對應的setter方法:例如:setIcon方法,有就直接調用這個setter方法給模型這個屬性賦值[self setIcon:dic[@"icon"]];
- 如果找不到setter方法,接着就會去尋找有沒有icon屬性,如果有,就直接訪問模型中的icon屬性,進行賦值,icon=dict[@"icon"];
- 如果找不到icon屬性,接着又會去尋找_icon屬性,如果有,直接進行賦值_icon=dict[@"icon"];
- 如果都找不到就會報錯:[<Flag 0X7fb74bc7a2c0> setValue:forUndefinedKey:]
-
如果對某個類,不允許使用KVC,可以通過設置 accessInstanceVariablesDirectly 控制。
// 在該類的內部,重寫此方法,外部使用KVC時,禁用沒有寫set get 方法的屬性值。 // 注意:對於 @property 定義的屬性可以 KVC+ -(BOOL)accessInstanceVariablesDirectly{ return NO; }
- 賦值檢查
// 在類的內部,進行檢查,不符合要求 返回NO ,提供外部參考。 - (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError{ if ([inKey isEqualToString:@"colors"] && [*ioValue isKindOfClass:[NSArray class]]) { return YES; } else { return NO; } } //用法: // 外部 使用時,先判斷是否符合要求,再使用KVC。 NSError *error; NSString *apoint = @"name"; if ([aPerson validateValue:&apoint forKey:@"_colors" error:&error]) { NSLog(@"可以賦值 apoint"); [aPerson setValue:apoint forKey:@"_colors"]; } else { NSLog(@"不可以賦值 apoint"); NSLog(@"%@",error.debugDescription); }
KVC內部的實現
比如說如下的一行KVC的代碼:
[site setValue:@"sitename" forKey:@"name"];
就會被編譯器處理成:
SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (site->isa,sel);
method(site, sel, @"sitename", @"name");
這下KVC內部的實現就很清楚的清楚了:一個對象在調用setValue的時候,(1)首先根據方法名找到運行方法的時候所需要的環境參數。(2)他會從自己isa指針結合環境參數,找到具體的方法實現的接口。(3)再直接查找得來的具體的方法實現。