iOS中KVO的巧妙使用及原理探究

YYKitNSObject+YYAddForKVO文件中以Block的形式包装了普通的KVO方法,觉得很是巧妙,由此引发了对KVO原理的探索。

1.NSObject+YYAddForKVO的代码实现

@interface _KVOWithBlockTarget_ : NSObject
@property(nonatomic, copy) void (^block)(__weak id obj, __weak id oldVal, __weak id newVal);
- (instancetype)initWithBlock:(void (^)(__weak id obj, __weak id oldVal, __weak id newVal))block;
@end

@implementation _KVOWithBlockTarget_
- (instancetype)initWithBlock:(void (^)(__weak id, __weak id, __weak id))block {
    if (self = [super init]) {
        self.block = block;
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {   
    if (!self.block) return;
    BOOL isPrior = [[change objectForKey: NSKeyValueChangeNotificationIsPriorKey] boolValue];
    if (isPrior) return; 
    NSKeyValueChange changeKind = [[change objectForKey: NSKeyValueChangeKindKey] integerValue];
    if (changeKind != NSKeyValueChangeSetting) return;
    id oldVal = [change objectForKey: NSKeyValueChangeOldKey];
    if (oldVal == [NSNull null]) oldVal = nil;
    id newVal = [change objectForKey: NSKeyValueChangeNewKey];
    if (newVal == [NSNull null]) newVal = nil;
    self.block(object, oldVal, newVal);
}
@end

static const int block_key;
@implementation NSObject (KVOWithBlock)
// 添加观察block
- (void)addObserverForKeyPath:(NSString *)keyPath block:(void (^)(id, id, id))block {
    _KVOWithBlockTarget_ *target = [[_KVOWithBlockTarget_ alloc] init];
    NSMutableDictionary *dic = [self _allNSObjectObserverBlocks];
    NSMutableArray *arr = dic[keyPath];
    if (!arr) {
        arr = [NSMutableArray new];
        dic[keyPath] = arr;
    }
    [arr addObject:target];
    [self addObserver: target forKeyPath: keyPath options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context: NULL];
}

// 移除该对象某个keyPath对应的Blocks
- (void)removeObserverBlocksForkeyPath: (NSString *)keyPath {
    if (!keyPath) return;
    NSMutableDictionary *dic = [self _allNSObjectObserverBlocks];
    NSMutableArray *arr = dic[keyPath];
    if (!arr) return;
    [arr enumerateObjectsUsingBlock:^(id target, NSUInteger index, BOOL *stop) {
        [self removeObserver:target forKeyPath:keyPath];
    }];
    [dic removeObjectForKey:keyPath];
}

// 移除该对象所有Blocks
- (void)removeAllObserverBlocks {
    NSMutableDictionary *dic = [self _allNSObjectObserverBlocks];
    [dic enumerateKeysAndObjectsUsingBlock:^(NSString *keyPath, NSArray *arr, BOOL *stop) {
        [arr enumerateObjectsUsingBlock:^(id target, NSUInteger index, BOOL *stop) {
            [self removeObserver:target forKeyPath:keyPath];
        }];
    }];
    [dic removeAllObjects];
}

// 管理观察集合  [keyPath: [target]...] (多个keyPath,每个keyPath多个观察者)
- (NSMutableDictionary *) _allNSObjectObserverBlocks {
    NSMutableDictionary *targets = objc_getAssociatedObject(self, &block_key);
    if (!targets) {
        targets = [NSMutableDictionary new];
        objc_setAssociatedObject(self, &block_key, targets, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return targets;
}
@end

分析:自定义_KVOWithBlockTarget_对象作为observer,然后自身通过数组字典来管理这些observer,包装非常巧妙,并未触及KVO的底层原理。

2.KVO原理分析

  • KVO是基于runtime机制实现的,当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(子类),A类的派生类名为NSKVONotifying_A。后面可能会应用到的方法objc_allocateClassPairobjc_registerClassPair
  • 每个类对象中都一个isa指针指向当前类,当一个类对象的属性第一次被观察时,那么系统会将isa指针指向动态生成的派生类,进而在赋值时执行的是派生类setter方法;而此时苹果为了隐藏该派生类,也重写了class方法,所以在执行[ojb class]方法时返回的依旧是原来的类。
  • KVO的观察通知则依赖于NSObject的两个方法 willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前,willChangeValueForKey:会被调用,这就记录旧值;而当发生改变之后,didChangevlueForKey:会被调用,继而observeValueForKey:ofObject:change:context: 也会被调用。所以可以预测伪码如下
- (void)setName:(id)name {
      // 记录旧值
      [self willChangeValueForKey:];
      [super setValue: name forKey: @"name"];
      // 记录新值
      [self didChangevlueForKey:];  
}
  • 以上,如果我们想手动触发KVO,可通过手动调用上面描述的两个方法
    // “手动触发self.name的KVO”,必写。
    [self willChangeValueForKey:@"name"];

    // “手动触发self.name的KVO”,必写。
    [self didChangeValueForKey:@"name"];
  • 原理图如下


    KVO原理图

3.自定义实现简单的KVO机制

NSObject+YYAddForKVO不同,这里我们完全自定义实现KVO机制,具体步骤如下

  1. 创建派生类,并仿照苹果隐藏派生类(重写class方法)
  2. 重写派生类的setter方法
  3. 类似YYAddForKVO,创建observer类来传递block(下面代码也是包装Block实现KVO)
#import "NSObject+KVO.h"
#import <objc/message.h>

NSString *const kYYKVOClassPrefix = @"YYKVOClassPrefix_";
NSString *const kYYKVOObservers = @"YYKVOObservers";

typedef void(^YYKVOBlock)(__weak id observer, NSString *observerdKey, __weak id oldVal, __weak id newVal) ;

//  observer类
@interface YYObserveationTarget: NSObject
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) YYKVOBlock block;

- (instancetype)initWithObserverForKey: (NSString *)key block: (YYKVOBlock)block;
@end

@implementation YYObserveationTarget
- (instancetype)initWithObserverForKey: (NSString *)key block: (YYKVOBlock)block {
    if (self = [super init]) {
        _key = key;
        _block = block;
    }
    return self;
}
@end

static NSString * setterForGetter(NSString *getter) {
    if (getter.length <= 0) return nil;
    // 首字母大写
    NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
    NSString * remainLetters = [getter substringFromIndex:1];
    return [NSString stringWithFormat:@"set%@%@:",firstLetter,remainLetters];
}
static NSString * getterForSetter(NSString *setter) {
    if (setter.length <= 0 || ![setter hasPrefix:@"set"]) return nil;
    
    NSRange range = NSMakeRange(3, setter.length - 4);
    NSString *key = [setter substringWithRange:range];
    // 首字母小写
    NSString *firstLetter = [[key substringWithRange:NSMakeRange(0, 1)] lowercaseString];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstLetter];
    return key;
}

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

static void kvo_setter(id self, SEL _cmd, id newVal) {
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    if (!getterName) return;
    
    id oldValue = [self valueForKey:getterName];
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 防止编译器多参报错
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    // 调用原类的setter方法(容易忘记)
    objc_msgSendSuperCasted(&superClazz, _cmd, newVal);
    
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kYYKVOObservers));
    //  Observer类来管理block的执行
    for (YYObserveationTarget *each in observers) {
        if ([each.key isEqualToString: getterName]) {
            // 在一个全局队列中执行block回调
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newVal);
            });
        }
    }
}


@implementation NSObject (KVO)
- (void)yy_addObserverForKey: (NSString *)key withBlock: (YYKVOBlock)block {
    
    // 0.key对应的setter等信息
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    
    // 1. 判断是否为派生类,如果不是则创建
    Class cls = object_getClass(self);
    NSString *clsName = NSStringFromClass(cls);
    // 1.1如果不包含前缀,说明不是派生类
    if (![clsName hasPrefix:kYYKVOClassPrefix]) {
        // 1.2 创建派生类
        cls = [self makeKVOClass];
        // 1.3 将isa指针指向派生类
        object_setClass(self, cls);
    }
    
    // 2.重写派生类的setter方法,判断是否已经重写过了
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(cls, setterSelector, (IMP)kvo_setter, types);
    }
    
    // 3.管理observers
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)kYYKVOObservers);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)kYYKVOObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    YYObserveationTarget *observer = [[YYObserveationTarget alloc]initWithObserverForKey: key block:block];
    [observers addObject: observer];
}

- (void)yy_removeObserverForKey: (NSString *)key {
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void*)kYYKVOObservers);
    NSMutableArray<YYObserveationTarget *> *infoToRemoveAry = [NSMutableArray array];
    for (YYObserveationTarget *observer in observers) {
        if ([observer.key isEqualToString:key]) {
            [infoToRemoveAry addObject:observer];
        }
    }
    [observers removeObjectsInArray:infoToRemoveAry];
}

- (Class)makeKVOClass {
    // 1.派生类类名
    NSString *kvoClsName = [kYYKVOClassPrefix stringByAppendingString:NSStringFromClass([self class])];
    Class kvoCls = NSClassFromString(kvoClsName);
    // 如果该类已经存在,则直接返回
    if (kvoCls) return kvoCls;
    
    Class originalCls = object_getClass(self);
    // 2. 动态创建该派生类,派生类继承自原类
    kvoCls = objc_allocateClassPair(originalCls, kvoClsName.UTF8String, 0);
    // 2.1 仿照苹果做法,重写class方法,隐藏该子类
    Method clsMethod = class_getInstanceMethod(originalCls, @selector(class));
    const char *types = method_getTypeEncoding(clsMethod);
    // kvo_class 返回父类类名,即返回原类,从而达到隐藏的效果
    class_addMethod(kvoCls, @selector(class), (IMP)kvo_class, types);
    // 2.2成对使用动态创建派生类
    objc_registerClassPair(kvoCls);
    
    return kvoCls;
}

- (BOOL)hasSelector: (SEL)selector {
    // 这里因为对象将isa指针指向了派生类,所以返回的应该是kvoCls
    Class cls = object_getClass(self);
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(cls, &methodCount);
    for (unsigned int i = 0 ; i < methodCount; i++) {
        SEL aSelector = method_getName(methodList[i]);
        if (aSelector == selector) {
            free(methodList);
            return YES;
        }
    }
    free(methodList);
    return NO;
}
@end

可以看到代码中大量应用了runtime知识,因而熟悉runtime对我们学习iOS原理有很大的帮助。

4.Swift4中KVO的使用方式

  1. 只有继承自NSObject类才能使用KVO
  2. 在Swift4中需要标记@objcMembersdynamic才能使用KVO
  3. 不在需要手动移除observer, 而是返回一个NSKeyValueObservation闭包,这带来了一个新的陷阱,我们需要主动去控制这个闭包的生命周期;如果没有对其强引用,该函数结束后闭包就会被回收。
@objcMembers class KVOClass: NSObject {
    dynamic var name: String
    init(name: String) {
        self.name = name
    }
}
 
class ViewController: UIViewController {
 
    var aClass: KVOClass!
    var ob: NSKeyValueObservation!
 
    override func viewDidLoad() {
        super.viewDidLoad()

        aClass = KVOClass(name: "kvo")
        ob = aClass.observe(\.name) { (ob, changed) in
            let new = ob.name
            print(new)
        }
        aClass.name = "swift4"
    }
}

以上是对KVO的一点小结,主要参考了以下的几篇文章
探究KVO的底层实现原理
如何自己动手实现 KVO
Swift 4新知:KVC和KVO新姿势

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容