天天看點

iOS底層探索之KVO(四)—自定義KVO

回顧

在上篇部落格已經自定義了

KVO

,但是還沒有完善,還有些問題需要解決,這麼本篇部落格就把自定義

KVO

進行完善。

iOS底層探索之KVO(四)—自定義KVO

1. 觀察者資訊儲存問題

在上一篇的部落格中,自定義

KVO

的簡單邏輯是已經實作了,但是這裡還是存在一個大的問題,就是如果我們要觀察多個屬性的時候,以及新值和舊值,都要觀察以及傳遞了

context

的情況下就無效了。

解決的辦法就是,我們需要儲存觀察者相關的資訊,那麼就建立一個新類

JPKVOInfo

儲存,代碼的實作如下:

typedef NS_OPTIONS(NSUInteger, JPKeyValueObservingOptions) {
    JPKeyValueObservingOptionNew = 0x01,
   JPKeyValueObservingOptionOld = 0x02,
};

@interface JPKVOInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) JPKeyValueObservingOptions options;
@property (nonatomic, strong) id context;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JPKeyValueObservingOptions)options context:(nullable void *)context;

@end

@implementation JPKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JPKeyValueObservingOptions)options context:(nullable void *)context {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.options  = options;
        self.context = (__bridge id _Nonnull)(context);
    }
    return self;
}

@end
           

注意

:

observer

的修飾要使用

weak

,弱引用,防止循環引用問題

那麼在

jp_addObserver

中資訊需要儲存觀察者資訊,如下代碼:

JPKVOInfo *info = [[JPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
    
    if (!observerArr) {
        observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
           

jp_setter方法裡面的邏輯修改之後如下代碼:

//1.通知觀察者,先拿到觀察者
    NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kjPKVOAssiociateKey));
    for (JPKVOInfo *info in observerArray) {//循環調用,可能添加多次。
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                //對新舊值進行處理
                if (info.options & jPKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & jPKeyValueObservingOptionOld) {
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    } else {
                        [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    }
                }
                [change setObject:@1 forKey:@"kind"];
                //消息發送給觀察者
                [info.observer jp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
            });
        }
    }
           
  • 在調用父類之前得先擷取舊的值。
  • 取出關聯對象數組資料,循環判斷調用。
  • jp_observeValueForKeyPath

    通知觀察者。

這個時候觀察多個屬性以及多次觀察就都沒問題了。

2. 移除觀察者

  • jp_removeObserver
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    [self jp_removeObserver:observer forKeyPath:keyPath context:NULL];
}

- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
    NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
    if (observerArray.count <= 0) {
        return;
    }
    
    NSMutableArray *tempArray = [observerArray mutableCopy];
    for (JPKVOInfo *info in tempArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            if (info.observer) {
                if (info.observer == observer) {
                    if (context != NULL) {
                        if (info.context == context) {
                            [observerArray removeObject:info];
                        }
                    } else {
                        [observerArray removeObject:info];
                    }
                }
            } else {
                if (context != NULL) {
                    if (info.context == context) {
                        [observerArray removeObject:info];
                    }
                } else {
                    [observerArray removeObject:info];
                }
            }
        }
    }
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //已經全部移除了
    if (observerArray.count <= 0) {
        //isa指回給父類
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}
           
  • 通過

    keyPath

    以及

    observer

    context

    确定要移除的關聯對象資料。
  • 當關聯對象中沒有資料的時候

    isa

    進行指回。

3. 函數式程式設計KVO

3.1 注冊與回調綁定

我們上面已經完成了

KVO

的自定義了,這和系統的

KVO

的實作都有一個問題,都需要三步曲。

jp_addObserver

jp_observeValueForKeyPath

都得分開寫,代碼分散,邏輯代碼和業務代碼分太開了,我們能不能用函數式程式設計思想,将他們放在一起呢?那麼試着去實作一下。

先為

KVO

分類添加一個

block

,并且在

addObserver

的函數裡面添加上這個

block

,這樣注冊和回調就可以在一起處理了。
  • 修改注冊方法為

    block

    實作:
typedef void(^JPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
......此處省略....
    //儲存觀察者資訊-數組
    JPKVOBlockInfo *kvoInfo = [[JPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
......此處省略.......
}
           

block

實作也儲存在

JPKVOBlockInfo

中,這樣在回調的時候直接執行

block

實作就可以了。

  • 回調邏輯:
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPBlockKVOAssiociateKey));
for (JPKVOBlockInfo *info in observerArray) {//循環調用,可能添加多次。
    if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        });
    }
}
           
在回調的時候直接将新值與舊值一起傳回。
  • 注冊調用邏輯:
[self.obj jp_addObserver:self forKeyPath:@"name" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
    NSLog(@"block: oldValue:%@,newValue:%@",oldValue,newValue);
}];
           

3.2 KVO自動銷毀

即使我們已經實作了注冊和回調綁定,但是在觀察者

dealloc

的時候仍然需要

remove

那麼怎麼能自動釋放不需要主動調用呢?

removeObserver

的過程中主要做了兩件事,移除關聯對象數組中的資料以及指回

isa

。關聯對象不移除的後果是會繼續調用回調,那麼在調用的時候判斷下

observer

存不存在,這樣來處理是否回調就可以了?核心就在指回

isa

了。

對dealloc方法進行Hook

+ (void)jp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
    if (!cls) {
        NSLog(@"class is nil");
        return;
    }
    if (!swizzledSEL) {
        NSLog(@"swizzledSEL is nil");
        return;
    }
    //類/元類
    Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
    Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
    Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
    if (!oriMethod) {//原始方法沒有實作
        // 在oriMethod為nil時,替換後将swizzledSEL複制一個空實作
        class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        //添加一個空的實作
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
           NSLog(@"imp default null implementation");
        }));
    }
    //自己沒有則會添加成功,自己有添加失敗
    BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    if (success) {//自己沒有方法添加一個,添加成功則證明自己沒有。
       class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else { //自己有直接進行交換
       method_exchangeImplementations(oriMethod, swiMethod);
    }
}

+ (void)load {
    [self jp_methodSwizzleWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}

- (void)jp_dealloc {
   // [self.obj jp_removeObserver:self forKeyPath:@""];
    [self jp_dealloc];
}
           
  • jp_dealloc

    中調用

    jp_removeObserver

    移除觀察者。
  • 但是我們發現有個問題是:被觀察者和

    keypath

    從哪裡來?這裡相當于是觀察者的

    dealloc

    中調用。
  • 是以可以通過在注冊的時候對觀察者添加關聯對象,儲存被觀察者和

    keyPath:

static NSString *const kJPBlockKVOObserverdAssiociateKey = @"JPKVOObserverdAssiociateKey";

@interface JPKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString  *keyPath;
@end
@implementation JPKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
    if (self=[super init]) {
        _observerd = observerd;
        _keyPath  = keyPath;
    }
    return self;
}
@end
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
    ……    
    //儲存被觀察者資訊
    JPKVOObservedInfo *kvoObservedInfo = [[JPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
    NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey));
    if (!observerdArray) {
        observerdArray = [NSMutableArray arrayWithCapacity:1];
    }
    [observerdArray addObject:kvoObservedInfo];
    objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
           
  • 執行個體化的

    kvoObservedInfo

    中儲存的是

    self

    ,也就是被觀察者。
  • 關聯對象關聯在observer也就是觀察者身上。

那麼現在在

dealloc

中周遊對其進行移除操作:

- (void)jp_dealloc {
    NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey));
    for (JPKVOObservedInfo *info in observerdArray) {
        if (info.observerd) {
            [info.observerd jp_removeObserver:self forKeyPath:info.keyPath];
        }
    }
    [self jp_dealloc];
}
           

我們這裡的方法的執行,隻是針對被觀察者沒有釋放的情況,如果釋放了

observerd

就不存在的情況下,我們是不需要調用

remove

處理的。

Hook的優化

在上面的做法中,有不合理的問題存在:就是在

+ load

Hook dealloc

方法是在

NSObject

分類中處理的,那麼意味着所有的類的

dealloc

方法都被Hook了。

那麼我們如何改進優化下

Hook

呢?

解決辦法就是:隻針對類進行

Hook dealloc

方法,是以可以将

Hook

延遲到

addObserver

中:

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
  ……
 //hook dealloc
 [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
           

但是對

dealloc hook

我們隻能夠

hook

一次,否則又交換回來了。

是以要麼做标記,要麼在建立

kvo

子類的時候進行

hoo

k。顯然在建立子類的時候更合适。代碼如下所示:

//申請類-注冊類-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
    //這裡重寫class後kvo子類也傳回的是父類的名字
    NSString *superClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJPBlockKVOClassPrefix,superClassName];
    Class newClass = NSClassFromString(newClassName);
    //類是否存在
    if (!newClass)  {//不存在需要建立類
        //1:申請類 父類、新類名稱、額外空間
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        //2:注冊類
        objc_registerClassPair(newClass);
        //3:添加class方法,class傳回父類資訊 這裡是`-class`
        SEL classSEL = NSSelectorFromString(@"class");
        Method classMethod = class_getInstanceMethod([self class], classSEL);
        const char *classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSEL, (IMP)_jp_class, classTypes);
        
        //hook dealloc
        [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
    }
    //4:添加setter方法
    SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)_jp_setter, setterTypes);
    
    return newClass;
}
           

3.3 Hook注冊和移除方法

對添加和移除的3個方法進行

Hook

處理:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self jp_methodSwizzleWithClass:self oriSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(jp_addObserver:forKeyPath:options:context:) isClassMethod:NO];
        [self jp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(jp_removeObserver:forKeyPath:context:)isClassMethod:NO];
        [self jp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(jp_removeObserver:forKeyPath:)isClassMethod:NO];
    });
}
           

因為

removeObserver:forKeyPath:

底層調用的不是

removeObserver:forKeyPath:context:

是以兩個方法都要

Hook

那麼我們又如何去怎麼判斷

observer

對應的

keyPath

是否存在。由于

observationInfo

存儲的是私有類,那麼可以直接通過

kvc

擷取值:

- (BOOL)keyPathIsExist:(NSString *)sarchKeyPath observer:(id)observer {
    BOOL findKey = NO;
    id info = self.observationInfo;
    if (info) {
        NSArray *observances = [info valueForKeyPath:@"_observances"];
        for (id observance in observances) {
            id tempObserver = [observance valueForKey:@"_observer"];
            if (tempObserver == observer) {
                NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
                if ([keyPath isEqualToString:sarchKeyPath]) {
                    findKey = YES;
                    break;
                }
            }
        }
    }
    return findKey;
}
           

Hook

方法的具體實作如下:

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//observer 觀察者已經添加了對應key的觀察,再次添加不做處理。
        return;
    }
    [self jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
        [self jp_removeObserver:observer forKeyPath:keyPath context:context];
    }
}

- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
        [self jp_removeObserver:observer forKeyPath:keyPath];
    }
}
           
這樣就解決了重複添加和移除的問題。

3.4 自動移除觀察者

重複添加和移除的問題已經解決了,那麼還有一個問題是

dealloc

的時候自動移除。這塊思路與自定義

kvo

相同,可以通過

Hook

觀察者的的

dealloc

實作。

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//observer 觀察者已經添加了對應key的觀察,再次添加不做處理。
        return;
    }
    NSString *className = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
    Class newClass = NSClassFromString(newClassName);
    if (!newClass) {//類不存在的時候進行 hook 觀察者 dealloc
        //hook dealloc
        [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
    }
    [self jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)jp_dealloc {
    [self jp_removeSelfAllObserverd];
    [self jp_dealloc];
}
           

如果

kvo

子類已經存在的時候,那麼說明已經

hook

過了。

dealloc

self.observationInfo

是擷取不到資訊的,

observationInfo

是存儲在被觀察者中的。是以還需要我們自己存儲下資訊。

static NSString *const kJPSafeKVOObserverdAssiociateKey = @"JPSafeKVOObserverdAssiociateKey";

@interface JPSafeKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString  *keyPath;
@property (nonatomic, strong) id context;

@end

@implementation JPSafeKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath context:(nullable void *)context {
    if (self=[super init]) {
        _observerd = observerd;
        _keyPath = keyPath;
        _context = (__bridge id)(context);
    }
    return self;
}

@end

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//observer 觀察者已經添加了對應key的觀察,再次添加不做處理。
        return;
    }
    NSString *className = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
    Class newClass = NSClassFromString(newClassName);
    if (!newClass) {//類不存在的時候進行 hook 觀察者 dealloc
        //hook dealloc
        [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
    }
    
    //儲存被觀察者資訊
    JPSafeKVOObservedInfo *kvoObservedInfo = [[JPSafeKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath context:context];
    NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPSafeKVOObserverdAssiociateKey));
    if (!observerdArray) {
        observerdArray = [NSMutableArray arrayWithCapacity:1];
    }
    [observerdArray addObject:kvoObservedInfo];
    objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPSafeKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //調用原始方法
    [self Jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
           

那麼我們現在就可以在

jp_dealloc

中主動的去調用移除

- (void)jp_dealloc {
    [self jp_removeSelfAllObserverd];
    [self jp_dealloc];
}

- (void)jp_removeSelfAllObserverd {
    NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kjPSafeKVOObserverdAssiociateKey));
    for (jPSafeKVOObservedInfo *info in observerdArray) {
        if (info.observerd) {
            //調用系統方法,已經hook了,走hook邏輯。
            if (info.context) {
                [info.observerd removeObserver:self forKeyPath:info.keyPath context:(__bridge void * _Nullable)(info.context)];
            } else {
                [info.observerd removeObserver:self forKeyPath:info.keyPath];
            }
        }
    }
}
           

這樣的話就會在

dealloc

的時候,就會自己主動清空,已經釋放掉的

observer

觀察者了。

4. 總結

  • 觀察多個屬性的時候,以及新值和舊值,都要觀察以及傳遞了

    context

    的情況下就無效,需要儲存觀察者相關的資訊,就可以建立一個新類

    JPKVOInfo

    儲存
  • 建立了觀察者就需要去移除觀察者
  • 可以通過

    Hook

    觀察者的的

    dealloc

    方法,實作自動移除。

更多内容持續更新

🌹 喜歡就點個贊吧👍🌹

🌹 覺得有收獲的,可以來一波,收藏+關注,評論 + 轉發,以免你下次找不到我😁🌹

🌹歡迎大家留言交流,批評指正,互相學習😁,提升自我🌹