iOS底層學習 - KVO探索之路

上一章節咱們講了KVC的使用和底層原理,並本身簡單實現了一個簡單的KVC,對KVC已經有了基本的瞭解,那麼這一章節,就來說一下,基於KVC的KVO是怎麼一回事html

傳送門☞iOS底層學習 - KVC探索之路編程

什麼是KVO

KVO:(Key-Value-Observer)是一種機制,也叫觀察者模式,該機制容許將其餘對象的特定屬性的更改通知給對象。對於應用程序中模型層和控制器層之間的通訊特別有用c#

KVO的使用

KVO的在平時的開發過程當中,使用也比較多。基本就是3個步驟:數組

  1. 觀察者註冊
  2. 觀察者接收通知
  3. 移除觀察者

下面逐步講解這3個步驟的使用安全

觀察者註冊

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context
複製代碼

使用上述代碼來進行觀察者的註冊,首先來看一下各參數的意義bash

  • observer:KVO 通知的對象,須要實現observeValueForKeyPath:ofObject:change:context:代理方法
  • keyPath: 被觀察者的屬性的名稱
  • options: 枚舉類型,主要是觀察的屬性的變化類型
  • context: 上下文,主要是傳遞給代理使用。

前兩個參數的意思比較好理解,對於後兩個參數,平時理解可能不那麼深入,咱們來重點解釋一下。多線程

1.optionsapp

NSKeyValueObservingOptionNew:觀察屬性變化後的新值框架

NSKeyValueObservingOptionOld:觀察屬性變化後的舊值函數式編程

NSKeyValueObservingOptionInitial:在屬性發生變化後當即通知觀察者,這個過程甚至早於觀察者註冊(使用較少)。簡單來講就是這個枚舉值會在屬性變化前先觸發一次回調。

NSKeyValueObservingOptionPrior:這個枚舉值會前後連續出發兩次 observeValueForKeyPath 回調。同時在回調中的可變字典中會有一個布爾值的 key - notificationIsPrior 來標識屬性值是變化前仍是變化後的。若是是變化後的回調,那麼可變字典中就只有 new的值了

在平時的開發中,咱們最嘗試用的就是NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld,從而進行邏輯編寫

2.context

The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.

A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.

[譯]addObserver:forKeyPath:options:context:消息中的上下文指針包含任意數據,這些數據將在相應的更改通知中傳遞迴觀察者。您能夠指定NULL並徹底依賴鍵路徑字符串來肯定更改通知的來源,可是這種方法可能會給對象的父類帶來問題,該對象的超類也出於不一樣的緣由而觀察相同的鍵路徑。

一種更安全,更可擴展的方法是使用上下文確保您收到的通知是發給觀察者的,而不是超類的。

經過閱讀文檔,咱們知道,context並非必需要傳遞的參數,若是不用時,咱們最好傳遞NULL,這時候在回調中的判斷就須要徹底根據objectkeyPath來進行判斷。可是不一樣若是有相同名稱keyPath時,判斷起來就要嵌套多層判斷語句,使用context能夠完美解決這個問題。

那麼怎麼使用比較好呢?

我推薦你們使用static void * XXContext = &XXContext;這種方法,靜態變量存儲着它的指針地址。在回調中能夠直接使用context == XXContex來進行判斷,方便快捷,並且更安全高效,擴展形強

觀察者接收通知

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

複製代碼

接收觀察者的對象,必須實現以上代理,才能接收到變化先後的值和參數。

參數的含義都比較好理解,和註冊時填寫的參數是基本一致的。change字典中就包含了觀察屬性變化先後的值,咱們所須要的數據也在裏面。object是被觀察的對象,context是註冊時傳遞的上下文,咱們通常用來作判斷使用.

移除觀察者

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
複製代碼

註冊和刪除是一一對應的關係。若是註冊了觀察者,則必須進行移除

一旦對某個對象上的屬性註冊了觀察者,能夠選擇在收到屬性值變化後取消註冊,也能夠在觀察者聲明週期結束以前(好比:dealloc 方法) 取消註冊,若是忘記調用取消註冊方法,那麼一旦觀察者被銷燬後,KVO 機制會給一個不存在的對象發送變化回調消息致使野指針錯誤。致使崩潰

KVO的自動與手動

經過上述的3大步驟註冊到一個觀察者後,當被觀察的keyPath出現變化時,對應的回調就能收到相關的數據,這屬於系統給咱們實現好的自動擋KVO。可是在平常開發中,咱們可能有一部分須要監聽,一部分不須要監聽,這時候咱們想要本身控制KVO變化,那咱們就須要實現手動擋KVO了。

實現手動擋的KVO,須要修改下面的方法。系統默認爲YES,若是咱們改成NO,則說明被觀察者須要手動進行觀察,才能出發回調了。咱們能夠再此方法中經過判斷key來進行自由的手動和自動的選擇

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
複製代碼

好比咱們的nick屬性須要進行手動處理,咱們能夠再他的set方法中添加willChangeValueForKeydidChangeValueForKey來標誌屬性即將發生變化和變化完成,這樣就實現了一個手動擋的KVO

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

集合類型的使用

當咱們觀察一個集合類型的屬性時,使用方法須要有細微的差異。是由於KVO是基於KVC的,因此必須有對應的set或者insert方法是才能夠。

好比咱們監聽dateArray可變數組(須要初始化)

[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
複製代碼

若是咱們直接調用add方法,這個時候是沒有走KVC的,因此此時KVO是監聽不到的。

[self.person.dateArray addObject:@"1"];
複製代碼

這個時候咱們應該使用KVC的方式,來對數組進行賦值,此時,就能夠監聽到數組的變化了

[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
複製代碼

一對多關係的使用

所謂一對多,就是被觀察的屬性的變化,取決於其餘多個屬性的變化。最多見的例子就是下載進度。下載進度=如今進度/總進度,因此當咱們觀察下載進度時,如今進度和總進度發生變化,都要觸發對應的回調。

好比,下載進入的get方法以下,能夠看到其收其餘兩個屬性影響

- (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];
}
複製代碼

當咱們監聽了downloadProgress屬性,writtenDatatotalData發生變化時,都能在回調中收到對應值。

[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
 
self.person.writtenData += 10;
self.person.totalData += 20;
 
複製代碼

KVO底層原理

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

[譯]自動鍵值觀察是使用稱爲isa-swizzling的技術實現的。 該isa指針,顧名思義,指向對象的類,它保持一個調度表。該分派表實質上包含指向該類實現的方法的指針以及其餘數據。 在爲對象的屬性註冊觀察者時,將修改觀察對象的isa指針,指向中間類而不是真實類。結果,isa指針的值不必定反映實例的實際類。 您永遠不要依靠isa指針來肯定類成員。相反,您應該使用該class方法肯定對象實例的類。

經過閱讀文檔內容,咱們能夠發現,KVO的實現原理很簡單,即isa-swizzling,把對象指向類的isa指向了一個中間類

中間類是什麼樣的?

經過下圖對象添加觀察者先後,isa指向的變化,能夠看到。在添加了觀察者的註冊以後,isa指向了一個名爲NSKVONotifying_XXX的類

中間類是原來類的子類麼?

咱們能夠經過以下代碼,查看有關該類的全部相關繼承的類。

- (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);
}
複製代碼

經過先後打印LGPerson的相關類,能夠明顯的看出,生成的中間類,是繼承自原來的類的,是其子類

中間類具體做用?

咱們知道,KVO是基於KVC的基礎上的,因此改變時必有getset方法。那麼是否是KVO是根據監聽其set方法來達到目的的呢,咱們能夠經過打印中間類重寫的方法來獲得驗證。

#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    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);
}
複製代碼

經過在添加觀察者先後方法列表,咱們能夠發現,繼承自原來類的中間類,主要重寫了set<keyPath>,class,dealloc,_isKVOA方法,從而達到KVO的實現。

方法的意義

set<keyPath>

KVO是基於KVC的,屬性發生變化時,必然要走進set方法,因此重寫此方法是必然的。KVO在重寫set方法後,內部要調用willChangeValueForKeydidChangeValueForKey方法,並在中間觸發observeValueForKeyPath:ofObject:change:context:回調,從而通知給觀察者進行操做。

class

重寫此方法是爲了對中間類進行假裝,經過對添加觀察先後,打印類的isa指向能夠得知,獲取到的元類仍是LGPerson,說明系統內部對class方法的重寫是對中間類的假裝,並在類調用calss方法時,仍是獲取到的原來的類。

dealloc

在添加KVO進行後,進行了isa_swizing,可是什麼時候給交換回來呢。

經過對dealloc方法打斷點,能夠得知,在觀察者銷燬後,對象的指向就會交換回來

那麼對象的isa交換回來後,中間類是否銷燬了呢,咱們能夠再打印一下相關的類和子類列表看一下,由此能夠發現,中間類並不會銷燬

_isKVOA

該方法就是用來標識是不是在觀察者狀態的一個標誌位。

自定義KVO

在瞭解了KVO的底層原理後,咱們仍是和KVC同樣,嘗試來簡單的來自定義一個KVO來加深一下印象。

首先咱們仍是仍是新建一個NSObject的分類用來處理KVO相關的邏輯,並對系統的KVO進行了函數式編程自動銷燬觀察者等優化,主體思路以下:

  1. 註冊觀察者
    • 判斷是否存在setter的方法
    • 動態生成繼承自原來類的中間類
    • 進行isa-swizzling到中間類
    • 創建Model並保存在數組或者字典中,用來輸出變化先後的數據,經過關聯對象保存
  2. 重寫setter並進行回調
    • set方法獲取getter方法的名稱 set<Key>:===> key,從而獲取到key
    • 根據keyPathKVC的方式獲取到舊值
    • 向父類發送消息(objc_msgSendSuper),歷來調用原來類的setter方法
    • 經過函數式編程思想,利用block回調,將保存的Model信息傳遞給觀察者
  3. 銷燬觀察者
    • 經過method-swizzling交換dealloc方法
    • 先將對象isa指回給原來的類
    • 執行原來類的dealloc原方法

以上爲自定義KVO的主體思路,下面咱們經過代碼來看一下。

註冊觀察者

首先咱們創建一個Model,用來保存咱們每次觀察者註冊時候的信息,用來返回舊值,保存傳遞block回調等。

static NSString *const kLGKVOPrefix = @"LGKVONotifying_";
static NSString *const kLGKVOAssiociateKey = @"kLGKVO_AssiociateKey";

@interface LGInfo : NSObject
@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString    *keyPath;
@property (nonatomic, copy) LGKVOBlock  handleBlock;
@end

@implementation LGInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block{
    if (self=[super init]) {
        _observer = observer;
        _keyPath  = keyPath;
        _handleBlock = block;
    }
    return self;
}
@end
複製代碼

接着提供註冊的方法,和系統的基本同樣,咱們應該加上自定義的前綴和block回調

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block;
複製代碼

下面是實現註冊的主要代碼

- (void)lg_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];
}
複製代碼

如下是各步驟的具體實現。

1.驗證是否存在setter方法

#pragma mark - 驗證是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    ✅// 獲取到當前的類
    Class superClass    = object_getClass(self);
    ✅// 獲取到當前的set方法IMP並進行判斷
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老鐵沒有當前%@的setter",keyPath] userInfo:nil];
    }
}

複製代碼

2.動態生成子類。經過原理的探究,咱們知道,中間類主要是重寫了3個方法,因此咱們在建立類的時候,要把這3個方法也動態建立出來,對原來類的方法進行重寫

#pragma mark - 動態生成子類
- (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;
}
複製代碼

重寫setter並進行回調

因爲咱們重寫了原來類的setter方法,因此新的setter方法要咱們本身來實現,道理也很簡單,就是獲取到新值後,發送給父類,即原來類,而後將存儲的Model取出,進行回調

tatic void lg_setter(id self,SEL _cmd,id newValue){
    ✅// 根據setter方法,獲取到key,並取出舊值
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    ✅// 消息轉發 : 轉發給父類
    // 改變父類的值 --- 能夠強制類型轉換
    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)),
    };
    lg_msgSendSuper(&superStruct,_cmd,newValue);
    
    ✅// 信息數據獲取並回調
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    
    for (LGInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}
複製代碼
#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];
}
複製代碼

銷燬觀察者

銷燬觀察者的這一步,咱們可使用常規的方法,即和系統實現同樣,提供一個移除觀察者的方法,在裏面進行isa的指回和關聯對象接觸等操做,可是這樣是比較麻煩的,每次註冊後,都須要記得手動去移除,代碼魯棒性差。

因此咱們想到,若是當前對象要進行銷燬了,那麼其中間類天然沒必要存在,這時候就能夠移除觀察者了。因此咱們hook原來類的dealloc方法,和咱們本身寫的方法進行交換,這樣就能夠添加上咱們本身的邏輯了。

具體代碼以下:

首先在load方法裏面進行交換,不要等到註冊的時候才進行交換

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self lg_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
    });
}

+ (BOOL)lg_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
    Class cls = self;
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!swiMethod) {
        return NO;
    }
    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    return YES;
}

複製代碼
- (void)myDealloc{
    ✅// 從關聯對象中刪除現有的keyPtah的Model
     NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    
    for (LGInfo *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) {
        ✅// isa指回給父類
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
    [self myDealloc];
}
複製代碼

至此,一個簡單的KVO咱們已經實現好了。可是這個並不完美,由於沒有考慮到多線程等狀況。你們能夠查看臉書的`FBKVOController`開源框架等進行加深學習

總結

KVO本質

  1. 當咱們給對象註冊一個觀察者添加了KVO監聽時,系統會修改這個對象的isa指針指向
  2. 在運行時,動態建立一個新的子類,NSKVONotifying_XXX類,將對象的isa指針指向這個子類
  3. 來重寫原來類的set方法;set方法實現內部會順序調用willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法
  4. 重寫class方法,對中間類進行假裝,返回原來類的class
  5. 在觀察者銷燬時,isa指回原來的類
  6. 觀察者銷燬後不刪除中間類

參考資料

KeyValueObserving 官方文檔

iOS底層原理探索—KVO的本質

iOS 底層探索 - KVO

相關文章
相關標籤/搜索