天天看點

iOS開發_KVO,KVC

2018.4.26提問:請描述一下你使用KVC,KVO的具體案例,以及你認為使用的時候需要注意的地方?

KVC:

具體案例:屬性指派,添加私有成員變量,字典轉模型

注意的地方:

  • KVC,使用setValuesForKeysWithDictionary:方法,該方法預設根據字典中每個鍵值對,調用setValue:forKey方法

    缺點:

    -1. 字典中的鍵值對必須與模型中的鍵值對完全對應,否則程式會崩潰

  • 2.字段比較多的話,手寫字典轉模型就很累

解決方案:

- 1.使用一個第三方控件MJExtension;可以做到字典轉模型,模型裡面還可以套結模型,也可以套接模型數組,功能比較完善和強大。

- 2.處理

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

- 封裝一個BaseModel 實作下面兩個方法,建立如下轉模型方法

// 将所有的資料轉換為字元串
-(void)setValue:(id)value forKey:(NSString *)key{

    if([value isKindOfClass:[NSNull class]]){
        value=nil;
    }else if([value isKindOfClass:[NSArray class]]){
    }else{
        value = [NSString stringWithFormat:@"%@",value];
    }
    [super setValue:value forKey:key];
}

// 對特殊字元 id 進行處理
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"Undefined Key: %@", key);
}

// 字典轉模型
-(id)initWithDic:(NSDictionary *)modelDic{

    self = [super init];
    if(self){
        [self setValuesForKeysWithDictionary:modelDic];
    }
    return self;

}
           

使用:

底層實作:

當一個對象調用setValue:forKey: 方法時,方法内部會做以下操作:

  • 1.判斷有沒有指定key的set方法,如果有set方法,就會調用set方法,給該屬性指派
  • 2.如果沒有set方法,判斷有沒有跟key值相同且帶有下劃線的成員屬性(_key).如果有,直接給該成員屬性進行指派
  • 3.如果沒有成員屬性_key,判斷有沒有跟key相同名稱的屬性.如果有,直接給該屬性進行指派
  • 4.如果都沒有,就會調用 valueforUndefinedKey 和setValue:forUndefinedKey:方法

KVO:

具體案例:

  • KVO有顯著的使用場景,當你希望監視一個屬性的時候,當處理屬性層的消息的事件時候,使用KVO,其他的盡量使用delegate。

優點:

  • 1.能夠提供一種簡單的方法實作兩個對象間的同步。例如:model和view之間同步;
  • 2.能夠對非我們建立的對象,即内部對象的狀态改變作出響應,而且不需要改變内部對象(SDK對象)的實作;
  • 3.能夠提供觀察的屬性的最新值以及先前值;
  • 4.用key paths來觀察屬性,是以也可以觀察嵌套對象;
  • 5.完成了對觀察對象的抽象,因為不需要額外的代碼來允許觀察值能夠被觀察

缺點:

  • 1.我們觀察的屬性必須使用strings來定義。是以在編譯器不會出現警告以及檢查;
  • 2.對屬性重構将導緻我們的觀察代碼不再可用;
  • 3.複雜的“IF”語句要求對象正在觀察多個值。這是因為所有的觀察代碼通過一個方法來指向;
  • 4.當釋放觀察者時不需要移除觀察者。

注意事項:

  • 潛在的問題有可能出現在dealloc中對KVO的登出上。KVO的一種缺陷(其實不能稱為缺陷,應該稱為特性)是,當對同一個keypath進行兩次removeObserver時會導緻程式crash,這種情況常常出現在父類有一個kvo,父類在dealloc中remove了一次,子類又remove了一次的情況下。不要以為這種情況很少出現!當你封裝framework開源給别人用或者多人協作開發時是有可能出現的,而且這種crash很難發現。不知道你發現沒,目前的代碼中context字段都是nil,那能否利用該字段來辨別出到底kvo是superClass注冊的,還是self注冊的?

    回答是可以的。我們可以分别在父類以及本類中定義各自的context字元串,比如在本類中定義context為@”ThisIsMyKVOContextNotSuper”;然後在dealloc中remove

    observer時指定移除的自身添加的observer。這樣iOS就能知道移除的是自己的kvo,而不是父類中的kvo,避免二次remove造成crash

KVC:鍵值編碼

主要作用是:

(1)通過鍵值路徑為對象的屬性指派。主要是可以為私有的屬性指派。

AppleViewController *appleVC = [[AppleViewController alloc]init];
[appleVC setValue:@"橘子" forKey:@"name"];
           

如果對象A的屬性是一個對象B,要設定對象B的屬性

[person setValue:@"旺财" forKeyPath:@"dog.name"];
           

(2)通過鍵值路徑擷取屬性的值。主要是可以通過key獲得私有屬性的值。

NSString *nameStr = [appleVC valueForKey:@"name"];
           

也可以通過keypath獲得值

NSString *dName = [person valueForKeyPath:@"dog.name"];
           

(3)将字典轉型成Model,方法:setValuesForKeysWithDictionary:

// 定義一個字典
    NSDictionary *dict = @{
                           @"name"  : @"jack",
                           @"money" : @"20.7",
                           };
    // 建立模型
    Person *p = [[Person alloc] init];

    // 字典轉模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"person's name is the %@",p.name);
           

注意:字典的key和Model的屬性一定要一一對應。否則會出現錯誤。比如person裡沒有name的屬性,系統報錯如下:

‘[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.’

二. KVO

1.KVO:鍵值觀察(key-value-observing)

KVO提供了一種觀察者的機制,通過對某個對象的某個屬性添加觀察者,當該屬性改變,就會調用”observeValueForKeyPath:”方法,為我們提供一個“對象值改變了!”的時機進行一些操作。

2.KVO原理

當某個類的對象第一次被觀察時,系統在運作時會建立該類的派生類,改派生類中重寫了該對象的setter方法,并且在setter方法中實作了通知的機制。派生類重寫了class方法,以“欺騙”外部調用者他就是原先那個類。系統将這個類的isa指針指向新的派生類,是以改對象也就是新的派生類的對象了。因而改對象調用setter就會調用重寫的setter,進而激活鍵值通知機制。此外派生類還重寫了delloc方法來釋放資源。

3.KVO的使用

(1)給對象的屬性添加觀察者

[appleVC addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
           

注: options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 傳回未改變之前的值和改變之後的值 context可以為空

(2)若該屬性發生改變,系統自動調用下面的方法:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //拿到新值/舊值,進行操作
    NSLog(@"newValue----%@",change[@"new"]);
    NSLog(@"oldValue----%@",change[@"old"]);
}
           

(3)取消監聽

-(void)dealloc
{
    [person removeObserver:self forKeyPath:@"test"];
}
           

4.KVO的使用場景

KVO用于監聽對象屬性的改變。

  (1)下拉重新整理、下拉加載監聽UIScrollView的contentoffsize;

  (2)webview混排監聽contentsize;

  (3)監聽模型屬性實時更新UI;

  (4)監聽控制器frame改變,實作抽屜效果。

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property(nonatomic,strong)Person * p;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _p =[[Person alloc]init];
    //添加觀察者
    [_p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"觀察到了");
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    _p.name [email protected]"hank";
}
           

成員變量是對成員屬性的一個封裝,每個成員變量都包含_name + getter + setter 方法

如果我們自己一個成員屬性,那麼使用observeValueForKeyPath就觀察不到值的變化,是以我們可以知道,觀察者觀察的是setter方法。

#import <Foundation/Foundation.h>

@interface Person : NSObject{
    @public
    NSString * _name;
}

//_name + getter + setter 方法!
//@property(nonatomic,copy)NSString * name;

@end
           

ViewController.h

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//    _p.name [email protected]"hank";
    _p->_[email protected]"hank";
}
           

KVO的底層實作原理:1.動态建立一個類NSKVONotyfing_Person,繼承自被繼承的類Person,重寫setter方法

2.改變P對象的類型!(子類類型)

#import "NSKVONotyfing_Person.h"

@implementation NSKVONotyfing_Person

-(void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}
@end