iOS底層探索 -- KVO探索

前言

  上一篇學習了KVC鍵值編碼的查找原理,而KVO(Key-Value Observing)在開發中也是用的比較多。本篇我們深入底層探索一下KVO的底層原理。

1. KVO初探

首先,先看一下,平常我們是怎麼寫KVO進行鍵值觀察的

如在某個類中,有一個 LGPerson 類型的屬性person,在這個類中對personname屬性進行觀察,代碼如下:


[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

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

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

這些都是我很熟悉的,但是要注意的是:添加的觀察者,一定要及時移除,否則,當對象釋放後,會造成野指針等問題。

接下來看一下KVO的一些細節問題。

1.1 context 的作用

查看添加觀察者API

addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context

前面三個參數,我們都很屬性,那麼最後一個void *類型的上下文context,有什麼有呢?

在平常的開發中,我們習慣的給傳NULL,那麼我們思考一個問題,

假如在一個類中,要對多個對象的同名屬性進行觀察,

比如:LGStudent繼承自LGPerson,而我們要在同一個類中對這個兩個屬性的name進行觀察,我們會怎麼做呢?

// ✅ 添加觀察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

// ✅ 監聽變化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LGViewController - %@",change);
    if (object == self.person) {
        if ([keyPath isEqualToString:@"name"]) {
            // 邏輯
        }
    } else if(object == self.student){
        if ([keyPath isEqualToString:@"name"]) {
            // 邏輯
        }
    }
    
}

// ✅ 移除
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.student removeObserver:self forKeyPath:@"name"];
}

這時我們就需要在監聽變化的方法中,寫很多判斷條件,然後來處理邏輯,這樣會很繁瑣。

通過查看文檔,其實我們可以在添加觀察者的時候,爲每個觀察到的鍵路徑創建一個不同的上下文,從而完全不需要進行字符串比較,從而可以更有效地進行通知解析,這是一個更加安全更加便利的方式

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




- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

1.2 自動觀察

在開發中,我們也會經常遇到一種情況,比如:需求頻繁改動,導致我們對某個屬性的觀察頻繁的刪除,然後重新,很是繁瑣。

其實,我們可以在被觀察的類中(比如上面對self.personname觀察,可以寫在LGPerson中),寫下面的代碼,

// 自動開關
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}

這個方法默認爲YES,我們可以設置爲NO,此時添加的觀察者會失效,在被觀察屬性發生變化是,需要手動通過兩個方法(willChangeValueForKey:didChangeValueForKey:)進行觀察

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

// 對 person 的 name 進行觀察
...

// 當 name 發生改變時
[self.person willChangeValueForKey:@"name"];
self.person.name  = @"null";
[self.person didChangeValueForKey:@"name"];

我們還可以通過下面的方式,對某個Key判斷,進而設置自動還是手動,設置爲NO的將不會被觀察。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

也可以在被觀察屬性的setter方法中,調用這兩個方法,進行手動觀察

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

1.3 多個因素影響

在開發中,也會遇到進度條的應用場景,而當前進度的佔比,是收兩個因素控制(當前下載量和總下載量)的,

比如下面的示例:

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;

@end

#import "LGPerson.h"

@implementation LGPerson

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

@end

[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    NSLog(@"LGViewController - %@",change);
    
}

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

在添加觀察者時,不光有正常的三部,還需要添加一個keyPathsForValuesAffectingValueForKey方法。

1.4 可變數組的觀察

在對可變數組進行觀察時,對數組進行修改時,不能通過調用addObject方法添加元素,應該通過下面的方式。

    // 數組變化不能通過這種方式
    // [self.person.dateArray addObject:@"1"];
    // KVO 建立在 KVC
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];

因爲KVO 是基於KVC的,而KVO的觀察是通過setter,可變數組的的獲取是通過mutableArrayValueForKey方法,不是像其他的setValeu:forKey方法。

2. KVO 原理分析

首先,我們定義一個LGPersonLGPerson中定義一個nickName的屬性。
然後對其觀察。

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;


- (void)sayHello;
- (void)sayLove;

@end

#import "LGPerson.h"

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


- (void)sayHello{
    
}
- (void)sayLove{
    
}

@end

在添加觀察者前後斷點調試,

分別打印self.person的類,如下:

發現在添加之後,self.person的類發生了變化,變成了NSKVONotifying_LGPerson

查看官方文檔,KVO底層,是對ias進行了swizzling。使對象的isa由原來的類指向了派生出來的NSKVONotifying_xxx

Automatic key-value observing is implemented using a technique called isa-swizzling.

那麼LGPersonNSKVONotifying_LGPerson是什麼關係呢?

我們通過RunTime API打印LGPerson父類的所有子類。

- (void)printClasses:(Class)cls{
    
    // 註冊類的總數
    int count = objc_getClassList(NULL, 0);
    // 創建一個數組, 其中包含給定對象
    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);
}

在添加觀察前後,打印結果如下:

由此可見,NSKVONotifying_LGPerson同樣繼承自LGPerson的元類。

我們知道,對一般類型的屬性進行觀察時,是觀察的這個屬性的setter,當添加觀察者時,會動態創建一個NSKVONotifying_xxx的類,那麼這個類中是否會對元類中的方法進行重寫呢?

打印方法列表如下:

從打印結果可以看出,動態創建的類中,重寫了setNickName:class
deallo_isKVOA

然後在觀察者銷燬時,將isa指向原來的類,在delloc方法中,打印self.person類:

那麼,動態生成的NSKVONotifying_xxx是否釋放了呢?

答案是否定的,因爲動態子類,創建成本太高,銷燬了會很浪費,而不銷燬方便下次快捷的使用

小結:

 1: 動態生成子類 : NSKVONotifying_xxx
 2: 觀察的是 setter
 3: 動態子類重寫了很多方法 setNickName (setter) class dealloc _isKVOA
 4: 移除觀察的時候 isa 指向回來
 5: 動態子類不會銷燬(創建成本太高,不釋放,方便下次使用)

3. 自定義 KVO 思路

系統的KVONSObject的一個分類NSObject(NSKeyValueObserving),凡是繼承自NSObject的類,都可以使用KVO

那麼接下來,嘗試自定義一個簡單的KVO

首先,可以自定義一點添加觀察者的方法,在這個方法中

1. 動態創建 NSKVONotifying_xxx 類。爲了防止錯誤,我們可以先檢測被觀察的 keyPath 是否有 setter 
2. 交換 isa 的指向,指向 NSKVONotifying_xxx
- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    
    // ✅ 1: 驗證是否存在setter方法 : 不讓實例進來
    [self judgeSetterMethodFromKeyPath:keyPath];
    // ✅ 2: 動態生成子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // ✅ 3: isa的指向 : LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    // ✅ 4: 保存信息(保存信息,方便拿到觀察者)
    LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

動態創建子類

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // ✅ 防止重複創建生成新類(因爲創建後,移除觀察者時不銷燬)
    if (newClass) return newClass;
    /**
     * 如果內存不存在,創建生成
     * 參數一: 父類
     * 參數二: 新類的名字
     * 參數三: 新類的開闢的額外空間
     */
    // ✅ 2.1 : 申請類
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // ✅ 2.2 : 註冊類
    objc_registerClassPair(newClass);
    // ✅ 2.3.1 : 添加class : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
    // ✅ 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
    
    // ✅ 2.3.3 : 添加dealloc
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);
    
    return newClass;
}

static void lg_dealloc(id self,SEL _cmd){
    
}

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"來了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // ✅4: 消息轉發 : 轉發給父類
    // 改變父類的值 --- 可以強制類型轉換
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // ✅ 5: 信息數據回調
    // ✅ 拿到觀察者
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 對新舊值進行處理
                if (info.options & LGKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & LGKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                // ✅ 2: 消息發送給觀察者
                SEL observerSEL = @selector(lg_observeValueForKeyPath:ofObject:change:context:);
                objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
            });
        }
    }
}

Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

自定義移除觀察者這方法

- (void)ll_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    
    for (LGKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    // ✅ 將指針指回
    if (observerArr.count<=0) {
        // 指回給父類
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

自定義監聽方法

- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{
    
}

4. KVO 函數式編程

在自定義添加KVO時,我們可以加入函數式編程的思想,在添加觀察者的時候,定義一個回調block,直接在被觀察屬性發生變化時,調用block,將其傳回。這樣就不用在寫監聽方法了。

對上面的lg_setter修改,將發送消息,改成回調block

- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block{
    
    // 1: 驗證是否存在setter方法 : 不讓實例進來
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 動態生成子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : LGKVONotifying_LGPerson
    object_setClass(self, newClass);
    // 4: 保存信息
    LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"來了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息轉發 : 轉發給父類
    // 改變父類的值 --- 可以強制類型轉換
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 5: 信息數據回調
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            // 回調block
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

// 添加觀察者
[self.person ll_addObserver:self forKeyPath:@"nickName" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        NSLog(@"%@-%@",oldValue,newValue);
    }];

其實,還可以將觀察者的指針指向原來的類的操作放到手動添加的delloc的實現中,這樣就可以自動釋放,不需要再調用移除方法

static void lg_dealloc(id self,SEL _cmd){
    Class superClass = [self class];
    object_setClass(self, superClass);
}
小結
1. 驗證是否存在 setter ,不讓實例變量儘量
2. 動態生成子類
    2.1 動態開闢一個新類 (NSKVONotifying_xxx)
    2.2 註冊類
    2.3 添加 class 方法,setter 方法,dealloc 方法
    2.4 使用關聯對象,保存觀察者
3. 修改 ISA 是指向,指向動態生成的類
4. 在重寫的 setter 中
    4.1 消息轉發給父類,調用父類的 setter ,給一種什麼也沒幹的假象
    4.2 通過發送消息,調用監聽方法(observeValueForKeyPath:)
        或者通過響應式編程,回調block

5. FBKVO 簡單分析

上面我們通過自定義KVOKVO底層原理有了一個系統的瞭解,其中肯定存在問題,而在一些開源網站上,有很多大牛自己封裝的KVO

接下來,簡單的瞭解一下FBKVO

FBKVO封裝了一個FBKVOController的中間層,添加了函數式編程的思想,可以通過下面的形式調用。

[self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(lg_observerAge)];
[self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
}];
[self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
}];

FBKVOController中,統一添加觀察者,統一處理,統一銷燬,如下圖,解決了循環引用的問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章