KVC與Runtime結合使用(案例)及其底層原理


一、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);
}

 

注意點:

  1. 在Person中我僅僅只是聲明了@class Car,而沒有引用#import "Car.h",然后在ViewController.m中便可以對其進行: [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];這樣子的賦值。所以說明KVC會去自動查找Car類進行賦值
  2. - (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)再直接查找得來的具體的方法實現。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM