iOS底層-22:KVO原理分析及使用

概述

KVO全程KeyValueObserving,是蘋果提供的一套鍵值觀察機制,它可以在對象指定屬性發(fā)生改變時接到通知。

基礎使用

KVO使用分為三個步驟:
1.通過addObserver:forKeyPath:options:context:注冊觀察者。觀察者可以接收keyPath屬性變化通知。
2.在觀察者中實現(xiàn)observeValueForKeyPath:ofObject:change:context:,以接收觀察屬性改變的通知消息
3.當觀察者不再需要接收消息時,使用removeObserver:forKeyPath:移除觀察者。在觀察者在內(nèi)存中釋放之前,必須移除,否者會crash

注冊函數(shù)

注冊觀察者時,需要傳入一個Option參數(shù),是一個枚舉值。

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    //接收新值
    NSKeyValueObservingOptionNew = 0x01,
    //接收舊值
    NSKeyValueObservingOptionOld = 0x02,
    //一旦注冊,立馬會調(diào)用一次
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
    //在變更前后都會發(fā)送通知,而不止是變更后
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};

還可以傳入context參數(shù),上下文參數(shù)可以傳遞任意數(shù)據(jù)指針,在通知方法中返回給觀察者。我們可以通過該參數(shù),來區(qū)分發(fā)生改變的類。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext=&PersonAccountInterestRateContext;

- (void)registerAsObserverForAccount:(Account*)account {

    [account addObserver:self  forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) ontext:PersonAccountBalanceContext];

    [account addObserver:self forKeyPath:@"interestRate" ptions:NSKeyValueObservingOptionNew | SNSKeyValueObservingOptionOld ontext:PersonAccountInterestRateContext];
}

監(jiān)聽方法

觀察者需要實現(xiàn)observeValueForKeyPath:ofObject:change:context:,KVO通知會通過這個方法傳遞,沒有實現(xiàn)會導致crashchange字典中存放KVO相關屬性的值,根據(jù)options傳入的枚舉值,可以取到數(shù)據(jù)。

change中還有NSKeyValueChangeKindKey,它也是一個枚舉值

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
     //觀察對象的值已經(jīng)更改
    NSKeyValueChangeSetting = 1,
    //觀察對象是否插入
    NSKeyValueChangeInsertion = 2,
    //觀察對象是否刪除
    NSKeyValueChangeRemoval = 3,
    //觀察對象是否替換
    NSKeyValueChangeReplacement = 4,
};

移除觀察者

你可以通過向觀察者對象發(fā)送removeObserver:forKeyPath:context:方法,指定觀察者,路徑和上下文來移除觀察者。

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

注意:

  • 如果你沒有注冊,就調(diào)用移除方法會導致NSRangeException,注冊和移除方法應該成對存在。如果這在你的程序中不可行,請將removeObserver放入try/catch中,處理潛在的異常。
  • 移除觀察者時,觀察者不會自動刪除自己,對已釋放的對象發(fā)送觀察通知會觸發(fā)內(nèi)存訪問異常。
  • 蘋果推薦我們在觀察者初始化是注冊(如 ininitviewDidLoad),在釋放期間移除(通常是dealloc)

自動開關與手動開關

KVO屬性發(fā)生改變時的調(diào)用一般是自動的,可以通過重寫automaticallyNotifiesObserversForKey:手動控制。(默認返回YES

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey{
  return YES;
}

手動控制nick屬性的KVO

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

注冊依賴鍵

有時候我們不僅僅是要觀察一個屬性,而是多個屬性。需要我們重寫keyPathsForValuesAffectingValueForKey:方法。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

數(shù)組 集合 觀察

監(jiān)聽數(shù)組等集合時,需要用mutableArrayValueForKey觸發(fā)通知

NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];

[transactions addObject:newTransaction];

KVO原理

  1. KVO只針對屬性,而不監(jiān)聽成員變量。
    準備代碼:
@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end
- (void)setNickName:(NSString *)nickName{
    _nickName = nickName;
}

- (void)viewDidLoad {
 [super viewDidLoad];
   
    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"Mike";
     self.person->name    = @"Tom";
}

#pragma mark - KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

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

打印情況:

實際情況:(null)-(null)
{
    kind = 1;
    new = Mike;
}

可以看出只是監(jiān)聽了屬性nickName,成員變量并沒有監(jiān)聽。

  1. 查看官方文檔,KVO的實現(xiàn)依賴于isa-swizzling技術,當觀察者注冊時,被觀察者的isa指針被修改,指向一個中間類而不是真正的類。

    代碼驗證:

    在圖中位置打上斷點,驗證注冊觀察者前后的isa指向

    注冊觀察者之后,self.personisa指向了NSKVONotifying_LGPerson。

查閱了相關資料,說NSKVONotifying_LGPersonLGPerson的子類。進一步驗證:


通過打印它的內(nèi)存結(jié)構(gòu),發(fā)現(xiàn)superClass正是LGPerson。

  1. 讓我們看一看NSKVONotifying_LGPerson里有什么方法
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

調(diào)用[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];方法打印結(jié)果為:

  1. NSKVONotifying_LGPerson類重寫setter方法做了什么?
    通過LLDB設置,觀察nickName屬性

watchpoint set variable self->_person->_nickName
不能使用點語法

self.person.nickName發(fā)生改變時,自動進入斷點。

也可以直接在setter方法中打斷點,查看堆棧

我們可以看到在setter方法前后,調(diào)用了NSKeyValueWillChangeNSKeyValueDidChangeBySetting

  1. 移除觀察者時,isa指針是否指回原本的類
    同樣,在removeObserver:forKeyPath:方法前后打印isa情況


    移除觀察者后,isa指針指向原本的類。

  2. 移除觀察者后,NSKVONotifying_LGPerson類是否從內(nèi)存中移除。
    通過以下方法,打印LGPerson的子類,看NSKVONotifying_LGPerson是否消失。

- (void)printClasses:(Class)cls{
    
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個數(shù)組, 其中包含給定對象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 獲取所有已注冊的類
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

removeObserver:forKeyPath:方法后打?。?/p>

classes = (
    LGPerson,
    LGStudent,
    "NSKVONotifying_LGPerson"
)
(lldb) 

NSKVONotifying_LGPerson依然作為LGPerson的子類存在,避免了再一次注冊KVO的重新開辟,節(jié)省性能。

自定義KVO

模擬KVO實現(xiàn)流程,自定義一份KVO代碼,實現(xiàn)了KVO自動銷毀。主要用來學習,加深記憶。

typedef void (^KVOHandle)(id observer, NSString  *  keyPath, id oldValue,id newValue);

@interface NSObject (LYKVO)


- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle;


@end

static NSString *const kLYKVOPrefix = @"LYKVONotifying_";
static NSString *const kLYKVOAssiociateKeyHandle = @"kLYKVO_AssiociateKeyHandle";
static NSString *const kLYKVOAssiociateKeyInfo = @"kLYKVO_AssiociateKeyInfo";

@implementation NSObject (LYKVO)

- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle {
    
    if (!observer || !keyPath || !options) return;
    // 1: 驗證是否存在setter方法 : 不讓實例進來
    [self checkSetterMethodFromKeyPath:keyPath];
    // 2: 動態(tài)生成子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3.將self 指向LGKVONotifying_Class
    object_setClass(self, newClass);
    
    
    //4.重寫setter方法 class_addMethod
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod  = class_getInstanceMethod([self class], setterSel);
    const char * setterType = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSel, (IMP)ly_setter, setterType);
    

    //5.保存block,用于傳值
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle), handle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //6.保存observer keyPath options 用于傳值
    LYKVOInfo *info = [[LYKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *infoArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo);
    if (!infoArr) {
        infoArr = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo, infoArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [infoArr addObject:info];
    
    
}

static void ly_setter(id self,SEL _cmd,id newValue) {

    SEL getSel = NSSelectorFromString(getterForSetter(NSStringFromSelector(_cmd)));
    id oldValue = objc_msgSend(self, getSel);

    //向父類發(fā)送一個setter消息
    void (*ly_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    ly_msgSendSuper(&superStruct, _cmd, newValue);


    //處理回調(diào)
    KVOHandle handle = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle));
    
    NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
    
    for (LYKVOInfo *info in mArr) {
        handle(info.observer,info.keyPath,oldValue,newValue);
    }
      
    
}
static Class ly_class(id self, SEL _cmd) {
    
    return class_getSuperclass(object_getClass(self));
}
static void ly_dealloc(id self, SEL _cmd) {
    
    //指針指回原本的類
    Class superClass = [self class];
    object_setClass(self, superClass);
    
    //處理數(shù)據(jù)
    NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
    if (mArr.count <= 0) {
        [mArr removeAllObjects];
        mArr = nil;
    }
    //執(zhí)行原本的dealloc方法
    objc_msgSend(self, _cmd);
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    
    //2.創(chuàng)建中間類
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLYKVOPrefix,oldClassName];
    
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
        
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    objc_registerClassPair(newClass);
    
    //3.重寫dealloc方法
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod  = class_getInstanceMethod([self class], deallocSel);
    const char * deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)ly_dealloc, deallocType);
    
    //4.重寫class方法
    SEL classSel = NSSelectorFromString(@"class");
    Method classMethod  = class_getInstanceMethod([self class], classSel);
    const char * classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)ly_class, classType);
    
    
    return newClass;
}
#pragma mark -驗證類的setter方法是否存在
- (void)checkSetterMethodFromKeyPath:(NSString *)keyPath {
    
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老鐵沒有當前%@的setter",keyPath] userInfo:nil];
    }
}
#pragma mark -只是簡單的拼接,并沒有考慮周全
#pragma mark - 從get方法獲取set方法的名稱 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
    
    if (getter.length <= 0) { return nil;}
    
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

#pragma mark - 從set方法獲取getter方法的名稱 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end

@interface LYKVOInfo : NSObject

@property (nonatomic, weak) NSObject  * observer;
@property (nonatomic, copy) NSString  *  keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options;

@end
@implementation LYKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options {
    if (self = [super init]) {
        self.observer = observer;
        self.keyPath = keyPath;
        self.options = options;
    }
    return self;
}
@end

推薦學習:
FBKVOController
GNN源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容