iOS底層探索-KVO

語言: CN / TW / HK

highlight: hybrid

1、KVC

鍵值編碼,通過Key名直接訪問物件屬性,由NSKeyValueCoding非正式協議啟用的機制

swift @interface LZPerson : NSObject {     @public     NSString *name; }

```swift

import "ViewController.h"

import "LZPerson.h"

@interface ViewController () @end

@implementation ViewController - (void)viewDidLoad {     [super viewDidLoad];     LZPerson *p = [LZPerson alloc];     p->name = @"lz";     [p setValue:@"kvcValue" forKey:@"name"];     NSLog(@"%@",[p valueForKey:@"name"]); } @end

// 列印結果 kvcValue `` - **KVC** 本質上是對 **NSObject**、**NSArray**、**NSDictionary**、**NSMutableDictionary**、**NSOrderedSet**、**NSSet** 等物件,實現NSKeyValueCoding分類,賦予它們Key-Value Coding`的能力;詳情參考 KVC文件

1.1、賦值流程

  • 首先會去找類的 set方法,如果找不到會去找 帶下劃線的set方法 ```swift @implementation LZPerson

    • (void)setName:(NSString *)name { self->name = @"setValue"; }

    • (void)_setName:(NSString *)name { self->name = @"_setValue"; } @end ```

    • 如果都找不到,則會看 +(BOOL)accessInstanceVariablesDirectly方法中的返回(預設為YESswift // 按照 _key、_isKey、key、isKey 的順序找屬性賦值 @interface LZPerson : NSObject { @public NSString *_name; NSString *_isName; NSString *name; NSString *isName; } @end
    • 返回YES時,會按照 _key_isKeykeyisKey 的順序找屬性賦值,如果 類中沒有上面的這些屬性 則會呼叫-(void)setValue:(id)value forUndefinedKey:(NSString *)key方法(自己實現一下,否則報錯)
    • 返回NO時,會直接呼叫 -(void)setValue:(id)value forUndefinedKey 方法 ```swift @implementation LZPerson
      • (BOOL)accessInstanceVariablesDirectly { return YES; } // 簡單實現一下防止崩潰
      • (void)setValue:(id)value forUndefinedKey:(NSString *)key { NSLog(@"%s",func); } @end ``` image.png

1.2、取值流程

  • 首先取值會按 getKeykeyisKey_key 的順序取 ```swift

    • (id)getName { return @"getGetNameValue"; }
    • (id)name { return @"getNameValue"; }
    • (id)isName { return @"getIsNameValue"; }
    • (id)_name { return @"get_NameValue"; }

    • (BOOL)accessInstanceVariablesDirectly { return YES; }

    • (id)valueForUndefinedKey:(NSString *)key { return @"valueForUndefineKey"; } ```

    • 找不到也會根據 +(BOOL)accessInstanceVariablesDirectly 返回值
    • 返回YES時,會按照 _key_isKeykeyisKey 的順序找屬性取值,如果 類中沒有這些屬性 則會呼叫-(id)valueForUndefinedKey:(NSString *)key方法(自己實現一下,否則報錯)
    • 返回NO時,直接呼叫 -(id)valueForUndefinedKey image.png image-2.png

1.3、API

swift // 通過 Key 讀取和儲存 - (nullable id)valueForKey:(NSString *)key; - (void)setValue:(nullable id)value forKey:(NSString *)key; swift // 通過 keyPath 讀取和儲存 - (nullable id)valueForKeyPath:(NSString *)keyPath; - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; ```swift // 預設返回YES,若沒有找到Set方法,按照_key、_iskey、key、iskey順序搜尋成員 + (BOOL)accessInstanceVariablesDirectly;

// KVC提供屬性值正確性驗證的API,它可以用來檢查set的值是否正確,為不正確的值做一個替換值或者拒絕設定新值並返回錯誤原因 - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString )inKey error:(out NSError *)outError;

// 這是集合操作的API,裡面還有一系列這樣的API,如果屬性是一個NSMutableArray,那麼可以用這個方法來返回 - (NSMutableArray )mutableArrayValueForKey:(NSString )key;

// 如果Key不存在,且KVC無法搜尋到任何和Key有關的欄位或者屬性,則會呼叫這個方法,預設是丟擲異常 - (nullable id)valueForUndefinedKey:(NSString *)key;

// 和上一個方法一樣,但這個方法是設值 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法時給Value傳nil,則會呼叫這個方法 - (void)setNilValueForKey:(NSString *)key;

//輸入一組Key,返回該組Key對應的Value,再轉成字典返回,用於將Model轉到字典 - (NSDictionary )dictionaryWithValuesForKeys:(NSArray )keys; ```

1.4、自定義KVC

相關方法 ```swift

import "NSObject+LZKVC.h"

import

@implementation NSObject (LZKVC) - (BOOL)lz_performSelectorWithMethodName:(NSString *)methodName value:(id)value{

if ([self respondsToSelector:NSSelectorFromString(methodName)]) {

pragma clang diagnostic push

pragma clang diagnostic ignored "-Warc-performSelector-leaks"

    [self performSelector:NSSelectorFromString(methodName) withObject:value];

pragma clang diagnostic pop

    return YES;
}
return NO;

}

  • (id)performSelectorWithMethodName:(NSString *)methodName{ if ([self respondsToSelector:NSSelectorFromString(methodName)]) {

pragma clang diagnostic push

pragma clang diagnostic ignored "-Warc-performSelector-leaks"

    return [self performSelector:NSSelectorFromString(methodName) ];

pragma clang diagnostic pop

}
return nil;

}

  • (NSMutableArray *)getIvarListName{

    NSMutableArray mArray = [NSMutableArray arrayWithCapacity:1]; unsigned int count = 0; Ivar ivars = class_copyIvarList([self class], &count); for (int i = 0; i<count; i++) { Ivar ivar = ivars[i]; const char ivarNameChar = ivar_getName(ivar); NSString ivarName = [NSString stringWithUTF8String:ivarNameChar]; NSLog(@"ivarName == %@",ivarName); [mArray addObject:ivarName]; } free(ivars); return mArray; } **KVC儲存**swift // 自定義KVC-儲存 - (void)lz_setValue:(nullable id)value forKey:(NSString *)key{

    // 1: 判斷key if (key == nil || key.length == 0) { return; }

    // 2: setter:set:→_set→setIs // key要大寫 NSString Key = key.capitalizedString; // 拼接方法 NSString setKey = [NSString stringWithFormat:@"set%@:",Key]; NSString _setKey = [NSString stringWithFormat:@"_set%@:",Key]; NSString setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

    if ([self lz_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*%@*",setKey); return; }else if ([self lz_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"*%@*",_setKey); return; }else if ([self lz_performSelectorWithMethodName:setIsKey value:value]) { NSLog(@"*%@****",setIsKey); return; }

    // 3: 判斷能否直接賦值例項變數,如果accessInstanceVariablesDirectly返回NO,奔潰 if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"LZUnknownKeyException" reason:[NSString stringWithFormat:@"*[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.*",self] userInfo:nil]; }

    // 4: 間接變數 // 獲取 ivar -> 遍歷 containsObjct // 4.1: 定義一個收集例項變數的可變陣列 NSMutableArray mArray = [self getIvarListName]; // _→_is→is NSString key = [NSString stringWithFormat:@"%@",key]; NSString _isKey = [NSString stringWithFormat:@"_is%@",Key]; NSString isKey = [NSString stringWithFormat:@"is%@",Key]; if ([mArray containsObject:_key]) { // 4.2: 獲取相應的 ivar Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String); // 4.3: 對相應的 ivar 設定值 object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:_isKey]) { Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String); object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:key]) { Ivar ivar = class_getInstanceVariable([self class], key.UTF8String); object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:isKey]) { Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String); object_setIvar(self , ivar, value); return; }

    // 5: 找不到,奔潰 @throw [NSException exceptionWithName:@"LZUnknownKeyException" reason:[NSString stringWithFormat:@"*[%@ %@]: this class is not key value coding-compliant for the key name.*",self,NSStringFromSelector(_cmd)] userInfo:nil]; } ```

KVC讀取 ```swift - (nullable id)lz_valueForKey:(NSString *)key{

// 1: 判斷key
if (key == nil  || key.length == 0) {
    return nil;
}

// 2: 找到相關方法get<Key>→<key> countOf<Key>  objectIn<Key>AtIndex
// key要大寫
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];

pragma clang diagnostic push

pragma clang diagnostic ignored "-Warc-performSelector-leaks"

if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
    return [self performSelector:NSSelectorFromString(getKey)];
}
else if ([self respondsToSelector:NSSelectorFromString(key)]){
    return [self performSelector:NSSelectorFromString(key)];
}
else if ([self respondsToSelector:NSSelectorFromString(isKey)]){
    return [self performSelector:NSSelectorFromString(isKey)];
}
else if ([self respondsToSelector:NSSelectorFromString(_key)]){
    return [self performSelector:NSSelectorFromString(_key)];
}
else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){

    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
    if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
        int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
        NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
        for (int i = 0; i<num-1; i++) {
            num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
        }
        for (int j = 0; j<num; j++) {
            id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
            [mArray addObject:objc];
        }
        return mArray;
    }
}

pragma clang diagnostic pop

// 3: 判斷是否能夠直接賦值例項變數
if (![self.class accessInstanceVariablesDirectly] ) {
    @throw [NSException exceptionWithName:@"LUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}

// 4: 找相關例項變數進行賦值
// 4.1: 定義一個收集例項變數的可變陣列
NSMutableArray *mArray = [self getIvarListName];
// _<key>→_is<Key>→<key>→is<Key>
// _name→_isName→name→isName
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
if ([mArray containsObject:_key]) {
    Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
    return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
    Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
    return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
    Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
    return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
    Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
    return object_getIvar(self, ivar);;
}

return @"";

} ```

2、KVO

  • KVO 全稱Key-Value Observing(鍵值觀察),是一種機制,允許物件在 其他物件的指定屬性發生更改時 收到通知

2.1、KVO 和 NSNotificatioCenter 的差異

  • KVO 只能用於監聽 物件屬性的變化NSNotificatioCenter 可以監聽任何你感興趣的東西
  • KVO 發出訊息由 系統控制NSNotificatioCenter開發者控制
  • KVO 自動記錄新舊值變化,NSNotificatioCenter 只能記錄開發者傳遞的引數

2.2、監聽過程

  • 註冊觀察者 swift [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    • 訊息中的 上下文指標context 包含任意資料,這些資料將在相應的更改通知中傳回給觀察者;可以指定NULL並完全依賴keyPath字串來確定更改通知的來源,但這樣可能會導致 父類由於不同原因也在觀察相同鍵路徑的物件時 出現問題
  • 屬性變化通知 ```swift
    • (void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary )change context:(void *)context{ if ([keyPath isEqualToString:@"name"]) { NSLog(@"%@",change); } } ```
  • 移除觀察者 swift [self.person removeObserver:self forKeyPath:@"name" context:NULL];

    • 如果被觀察者是單例,那麼如果被觀察者所在介面銷燬時不移除觀察者會崩潰(被觀察者未釋放,值改變方法還要呼叫,但介面被釋放,這個方法找不到了所以崩潰)
  • 設定 context上下文,區分通知來源 ```swift static void PersonNickContext = &PersonNickContext; static void PersonNameContext = &PersonNameContext;

    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext]; [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];

    • (void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary )change context:(void *)context{ if (context == PersonNickContext) { NSLog(@"nick:%@",change); return; }

      if (context == PersonNameContext){ NSLog(@"name:%@",change); return; } } ```

2.3、手動關閉KVO、手動觸發KVO

  • +(BOOL)automaticallyNotifiesObserversForKey手動關閉KVO ```swift
    • (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { return NO; } ```
  • willChangeValueForKeydidChangeValueForKey手動觸發KVO swift [LZPerson willChangeValueForKey:@"name"]; _name = name; [LZPerson didChangeValueForKey:@"name"];

2.4、監聽可變陣列

```swift self.person.dateArray = [NSMutableArray arrayWithCapacity:1]; [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

  • (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event{ // 這種寫法不能收到KVO通知,因為KVO基於KVC,訪問 集合物件 有三種不同的代理方法 // if(self.person.dateArray.count == 0){ // [self.person.dateArray addObject:@"1"]; // } // else{ // [self.person.dateArray removeObjectAtIndex:0]; // }

    if(self.person.dateArray.count == 0){ [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"]; } else{ [[self.person mutableArrayValueForKey:@"dateArray"] removeObjectAtIndex:0]; } }

  • (void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary )change context:(void *)context{ NSLog(@"%@",change); }

  • (void)dealloc{ [self.person removeObserver:self forKeyPath:@"dateArray"]; } ```

  • 集合物件訪問定義的三種不同的代理方法
    • mutableArrayValueForKey:和 mutableArrayValueForKeyPath:
    • mutableSetValueForKey:mutableSetValueForKeyPath:
    • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:
  • 會列印 NSKeyValueChange 型別的 kind,表示鍵值變化的型別,執行addObject時,kind 列印值為 2;執行removeObjectAtIndex時,kind 列印值為 3 swift /* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information. */ typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, //賦值 NSKeyValueChangeInsertion = 2, //插入 NSKeyValueChangeRemoval = 3, //移除 NSKeyValueChangeReplacement = 4, //替換 };

2.5、技術細節

  • 自動鍵值觀察是使用稱為isa-swizzling的技術實現
  • isa 指標指向物件的類,它保持一個排程表,該排程表主要包含指向類實現的方法的指標,以及其他資料
  • 當觀察者 註冊觀察物件的某屬性 時,被觀察物件的 isa 指標被修改指向中間類而不是真正的類;因此,isa 指標的值不一定反映例項的實際類
  • 永遠不要依賴 isa 指標來確定類成員身份,應該使用該 class 方法來確定物件例項的類

2.6、底層原理

isa改變

```swift - (void)viewDidLoad { [super viewDidLoad];

self.person = [[LZPerson alloc] init];
NSLog(@"新增KVO觀察者之前:%s", object_getClassName(self.person));
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"新增KVO觀察者之後:%s", object_getClassName(self.person));

}

// 列印結果 新增KVO觀察者之前:LZPerson 新增KVO觀察者之後:NSKVONotifying_LZPerson `` 1. 當呼叫addObserve方法時,系統動態生成當前類的子類NSKVONotifying_類名(**當前類的子類**) 2. 物件會將 **isa指標指向這個子類**,這個子類會生成對應的 **set方法(setName)**、**構造方法**、**dealloc**、**_isKVOA(標記是否為KVO生成的中間類)** 3. set方法中會呼叫willChangeValueForKeydidChangeValueForKey`兩個方法 4. 當註冊被移除時,物件將isa指標指回正常

  • NSKVONotifying_類名中的方法 ```swift

    • (void)viewDidLoad { [super viewDidLoad];

      self.person = [[LZPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];

      unsigned int intCount; Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LZPerson"), &intCount);

      for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {

      Method method = methodList[intIndex];
      NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
      

      } }

    // 列印結果 SEL:setNickName:,IMP:0x18a5d8520 SEL:class,IMP:0x18a5d6fd4 SEL:dealloc,IMP:0x18a5d6d58 SEL:_isKVOA,IMP:0x18a5d6d50 - 因為類的屬性有set方法,而 **成員變數沒有set方法**,因此`KVO不能監聽成員變數`;如果一定要監聽成員變數,需要 **使用KVC觸發**swift [self.person addObserver:self forKeyPath:@"_sex" options:(NSKeyValueObservingOptionNew) context:NULL]; //直接賦值無法觸發KVO,要用KVC //self.person->_sex = @"male"; [self.person setValue:@"male" forKey:@"_sex"]; ```

  • 可以參考FBKVOController

參考文章:https://www.yuque.com/u12101430/ag8prt/kgm2qe#DA03F