KVC的理解、與runtime結合應用及其底層原理

一、KVC的概念理解及常用方法

概念

KVC(Key-Value Coding)顧名思義,就是鍵值編碼的意思。
iOS中,KVC就是通過使用屬性的名稱間接性來訪問屬性的方法,通俗一點的理解就是可以通過對象屬性名稱(Key)直接給屬性值(Value)編碼(Coding)“編碼”可以理解爲“賦值”。

這個方法可以不通過getter/setter方法來訪問對象的屬性。因爲一個類的成員變量如果沒有提供getter/setter的話,外界就失去了對這個變量的訪問渠道。而KVC則提供了一種訪問的方法,這個在某些場合會很有威力。例如,直接修改系統控件的內部屬性,並且KVC還是KVOCoreData的技術基礎,他們都是利用了OC的動態性。

KVC常用的方法

   - (void)setValue:(nullable id)value forKey:(NSString *)key; // 爲對象的屬性賦值
   - (id)valueForKey:(NSString *)key;  // 根據key取值
   - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 爲對象的屬性賦值(包含了setValue:forKey:的功能,並且還可以對對象內的類的屬性進行賦值)
   - (nullable id)valueForKeyPath:(NSString *)keyPath; // 根據keyPath取值
   - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues; // 對模型一次性賦值,前提是必須聲明好所有對應的屬性(key)

現在我們用例子簡單的使用上面提到的KVC常用到的方法
創建一個YGPerson和YGCar類,頭文件分別爲:

YGPerson.h

YGPerson.h

YGCar

YGCar.h

ViewController

ViewController.h

從上面三張圖片的代碼,我們分析一下:
YGPerson和YGCar兩個類中,我們都是簡單的聲明瞭屬性,但是YGPerson中,我們用@class引入了YGCar的頭文件,將YGCar這個類的對象聲明成我們的屬性。而通過setValue: forKey:setVaule: forKeyPath:分別能給YGPerson中的name屬性和YGCar中的price屬性賦值。
並且通過打印結果,我們知道KVC通過value:forKey可以讀取了屬性的值,也進一步印證了我們開始的總結:KVC可以通過屬性的名稱間接的訪問屬性的方法,能賦值也能取值。

從下圖來看,我們使用setValue: forKey:給price賦值時就會報
"[setValue:forUndefindKey:]:this is not key value coding-compliant for key car.price"的錯誤,意思是說在setValue:forUndefinKey 這方法中找不到car.price的屬性。所以我們就能知道setValue:forKey此方法是在YGPerson類中無法找到car.price這個屬性,而setValue:forKeyPath是通過.號來把一個一個key鏈接起來,這樣就可以根據這個路徑訪問下去
image.png

通過上面的分析,KVC的注意點

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 forKey:(NSString *)key;
你會發現value的值必須是id,也就是說不能傳基本數據類型,必須是指針類型的變量,所以使用基本數據類型的時候,要裝箱成爲NSNumber類型。

3、key和keyPath的區別:
keyPath方法是集成了key的所有功能,也就是說對一個對象的一般屬性進行賦值、取值,兩個方法是通用的,都可以實現。但是對對象中的對象的屬性進行賦值,只有keyPath能夠實現

4、當key的值是沒有定義的,valueForUndefinedKey:這個方法會被調用,如果你自己寫了這個方法,key的值出錯就會調用到這裏來

5.、因爲類key反覆嵌套,所以有個keyPath的概念,keyPath就是用.號來把一個一個key鏈接起來,這樣就可以根據這個路徑訪問下去

setValuesForKeysWithDictionary:的巧妙使用(字典轉模型)

-(instancetype)initWithDict:(NSDictionary *)dict{
         if (self = [super init]) {
               [self setValuesForKeysWithDictionary:dict]; 
          } 
         return self;
}

注意點:

  • 字典轉模型的時候,字典中的某一個key一定要在模型中有對應的屬性
  • 如果一個模型中包含了另外的模型對象,是不能直接轉化成功的。
  • 通過kvc轉化模型中的模型,也是不能直接轉化成功的
  • 底層還是調用了setValue: forKey:

二、KVC的應用

1、修改系統控件的內部屬性(Runtime+KVC)

例如,界面要設計成這樣
1
但我們平時做輪播圖的效果是這樣的,這顯然不是我們的效果,但是要如何修改UIPageControl才能改成我們想要的橫線的效果呢?
2
那我們試想一下,UIPageControl默認的圖片就是一個圓點,那這個圓點是可能是一張圖片,它的屬性應該會有類型是UIImage這樣的屬性,那我們查看一下他的頭文件,看是否存在此類型的屬性。

3
從頭文件中,我們找不到我們想要相關的UIImage或UIImageView這樣的屬性類型,那我們想深入的查看一下UIPageControl.m文件裏的屬性,可能就是在.m文件裏的私有屬性,那我們如何能看到.m文件的成員屬性呢?這就要通過runtime遍歷出UIPageControl所有屬性(包括私有屬性)。

/**
 *  運行時(runtime),獲取所有成員變量
 */
- (void)getMemberVariables
{
    unsigned ivarCount;
    Ivar *ivars = class_copyIvarList([UIPageControl class], &ivarCount);

    for (int i = 0; i < ivarCount; i ++) {

        NSString *varibale = [NSString stringWithUTF8String:ivar_getName(ivars[i])];

        NSLog(@"%@",varibale);
    }
}

我們通過此方法打印出UIPageControl的所有屬性,發現有_pageImage和_currentPageImage 兩個屬性是符合我們修改當前pageControl的圖片和默認pageControl的圖片,如下我們就能修改了我們想要的UIPageControl的圖片了。

4

通過KVC來修改
UIPageControl *pageControl = [[UIPageControl alloc ] init];
[pageControl setValue:[UIImage imageNamed:%@"nomal.png"] forKey:@"_pageImage"];
[pageControl setValue:[UIImage imageNamed:%@"selected.png"] forKey:@"_currentPageImage"]

2、在xib/Storyboard中,也可以使用KVC,下面是在xib中使用KVC把圖片邊框設置成圓角

6

###3、在對模型轉換時,會出現警告。
例如:我們要將下列字典數據轉化爲模型

{
     "id"    :"23"
     "name"  :"zhangrm"
     "age"   :"25"
     "like"  :"oc"
}

當我們在模型類中定義時:如下。我們發現id是oc中的關鍵字,會報警告⚠️

   @property (nonatomic, strong) NSString *id;
   @property (nonatomic, strong) NSString *name;
   @property (nonatomic, strong) NSString *age;
   @property (nonatomic, strong) NSString *like;

雖然可以使用以下方法,對模型中的成員變量進行統一設置,但是出現警告總歸是不好的:

- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

既然這樣,可以選擇手動一個個去實現。但是這樣在數據少的時候可以試試,在數據比較多時就不太現實了,程序的可擴展性也不好。
兩種解決方法:

方法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:就不會出現模型中找不到對應的成員變量的錯誤了,但是必須注意,在使用setValuesForKeysWithDictionary,屬性必須和字典中的key一一對應。

方法二:使用rumtime

+ (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”];
  • 如果都找不到就會報錯:[ setValue:forUndefinedKey:]
  • 如果對某個類,不允許使用KVC,可以通過設置 accessInstanceVariablesDirectly 控制。

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)再直接查找得來的具體的方法實現。

發佈了30 篇原創文章 · 獲贊 8 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章