程序员iOS

iOS OC KVO使用+原理+自定义

2018-12-15  本文已影响3人  1江春水

首先先抛出来几个问题:

1、什么是KVO
2、KVO能做什么
3、如何使用
4、能否根据系统的KVO机制,自己手写一个KVO

讨论
1、KVO能监听成员变量吗?
2、KVO能监听Array的count属性吗

在讨论KVO之前,先看下官方文档给的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.

1、什么是KVO

KVO俗称键值观察(key-value observe),下边是官网给的定义

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

翻译过来就是:键值观察是当被观察的对象属性发生改变时,会通知到观察对象的一种机制。
官网还有一句话说,想要了解KVO,你必须了解KVC才可以。

2、KVO能做什么

1、可以监听系统对象属性,比如 scrollView的contentOffset属性,来监听页面的滑动;

2、可以监听字符串的改变,当监听的字符串改变时,来做一些自定义的操作;

3、可以监听按钮等带有状态的基础控件

4、等等

3、如何使用

使用可分为自动监听和手动监听.

先说自动监听:
分三步:
1、监听属性
2、处理改变的属性
3、在dealloc移除监听(当页面销毁,监听没有被移除,会导致崩溃)

代码:自定义一个TTObject对象,这个对象暴露出一个可读写name属性,在VC内部监听这个name属性的改变。

添加监听
self.obj = [[TTObject alloc] init];
//NSLog(@"obj = %@ ",object_getClass(self.obj));
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//NSLog(@"obj = %@ ",object_getClass(self.obj)); //类变了
处理改变的属性
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    NSLog(@"---- %@ ---- ",change);
}

移除监听
- (void)dealloc {
    [self.obj removeObserver:self forKeyPath:@"name"];
}

说一下各个参数分别代表什么意思:

/**
 添加监听
 @param observer 被监听的对象
 @param keyPath 被监听对象的属性名,不可为空,为空崩溃
 @param options 有4中,old new initial prior
 @param context 上下文
 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
手动监听
官网给的场景:
In some cases, you may want control of the notification process, for example, to minimize triggering notifications that are unnecessary for application specific reasons, or to group a number of changes into a single notification. Manual change notification provides means to do this.

使用:

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];//会调用 observeValueForKeyPath
    _name = name;
    [self didChangeValueForKey:@"name"];//会调用 observeValueForKeyPath
}

/**
 bool
 @param key 需要手动实现的 keypath
 @return 其他使用 [super automaticallyNotifiesObserversForKey:key]
 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

ps:对需要手动实现监听的属性值,需要分别调用 willChangeValueForKey 和 didChangeValueForKey两个方法,必须成对出现。而且如果不实现 automaticallyNotifiesObserversForKey这个方法,observeValueForKeyPath这个方法会调用两次,影响性能。

场景1 To-One Relationships

还有这样的场景,一个对象的某个属性依赖于对象的其他若干属性,例如:一个人的名字fullName 由 firstName 和 lastName组成;其中有一个value改变,就会被监听到。

在这里有两种办法指定被观察对象的属性所依赖的其他若干属性。
1、一种是重写系统方法 keyPathsForValuesAffectingValueForKey 返回NSSet实例,内部就是依赖的属性字符串。
2、另一种方法是实现 +keyPathsForValuesAffecting<Key>这个类方法,后边的<key>是你当前对象的属性,xcode会自动提示,同样返回一个NSSet实例,内部是依赖的属性字符串。

注意

1、需要重写 被观察对象属性的getter方法;

2、当有其中一个对象属性改变时,同样会响应监听

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

返回依赖代码如下:

方法1
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *set = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key  isEqualToString:@"fullName"]) {
        NSArray *affectArr = @[@"firstName",@"lastName"];
        set = [set setByAddingObjectsFromArray:affectArr];
    }
    return set;
}

方法2:方法末尾FullName 为系统自动提示。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

4、能否根据系统的KVO机制,自己手写一个KVO

从官方文档给的介绍来看,监听一个对象的属性,系统会通过OC语言的运行时机制来动态的创建这个对象的一个子类,系统创建的是带有NSKVONotifying_前缀的类,然后这个子类重写 被观察对象属性的 setter 方法,重写的setter方法会负责 在调用原对象的setter方法之前和之后会通知所有观察值的改变,然后改变被观察对象的isa指针来指向子类,从而回调 observeValueForKeyPath这个系统方法达到监听对象属性的目的。
不仅如此,Apple还重写了 -class 方法,企图隐瞒内部所做的操作。

手动实现KVO

最终我们想要的是这样的:

[obj tt_addObserver:obj keyPath:@"name" observerBlock:^(id  _Nonnull observer, id  _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {

}];

为NSObject写一个分类,跟系统方法类似,也是一句代码实现,并且改变前后的新旧值、观察对象、观察keypath在block返回。

总体分为四步:

首先创建一个NSObject的分类:

typedef void (^TTObserverBlock)(id observer,id keyPath, id oldValue,id newValue);

@interface NSObject (KVO)

- (void)tt_addObserver:(NSObject *)observer keyPath:(NSString *)keyPath observerBlock:(TTObserverBlock)block;

- (void)tt_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

来看下addObserver的内部实现

- (void)tt_addObserver:(NSObject *)observer keyPath:(NSString *)keyPath observerBlock:(TTObserverBlock)block {
    //1、检查监听的对象key 有没有setter方法,没有就抛出异常
    SEL setter = NSSelectorFromString(setterWithGetter(keyPath));
    Method method = class_getInstanceMethod([self class], setter);
    if (!method) {
        NSString *reason = [NSString stringWithFormat:@"obj %@ do not have a setter for key(%@)",self,keyPath];
        NSException *exception = [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
        @throw exception;
        return;
    }
    
    //2、检查被监听对象是不是一个KVO类,如果不是,创建一个继承于当前类的一个子类,并把isa指针指向当前类
    Class class = object_getClass(self);
    NSString *classStr = NSStringFromClass(class);
    if (![classStr containsString:TTKVOClassPrefix_]) {
        class = [self changeClass:classStr];//创建新类
        object_setClass(self, class);//改变原对象的isa指针
    }
    
    //3、检查对象的KVO类有没有重写过setter方法,没有的话就重写
    if (![self hasSelector:setter]) {
        const char *types = method_getTypeEncoding(method);
        //给创建的kvo类添加setter方法
        class_addMethod(class, setter, (IMP)kvo_setter, types);
    }
    //4、添加这个观察者
    TTObserverInfo *info = [[TTObserverInfo alloc] initWithObserver:observer keyPath:keyPath observerBlock:block];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, &kTTKVOObserver);
    if (!observerArr) {
        observerArr = @[].mutableCopy;
        objc_setAssociatedObject(self, &kTTKVOObserver, observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [observerArr addObject:info];
}
/**
 获取setter方法
 @param getter keypath
 @return setter
 */
NSString * setterWithGetter(NSString *getter) {
    if (getter.length <= 0 || getter == nil) {
        return nil;
    }
    NSString *uppterStr = [[getter substringToIndex:1] uppercaseString];
    NSString *remindStr = [getter substringFromIndex:1];
    return [NSString stringWithFormat:@"set%@%@:",uppterStr,remindStr];
}

- (Class)changeClass:(NSString *)originClassName {
    NSString *newClassStr = [TTKVOClassPrefix_ stringByAppendingString:originClassName];
    Class newClass = NSClassFromString(newClassStr);
    if (newClass) {//有新创建的类,直接返回
        return newClass;
    }

    Class originClass = object_getClass(self);
    //用带有KVO前缀的字符串 创建一个新类
    Class newClassCreate = objc_allocateClassPair(originClass, newClassStr.UTF8String, 0);
    
    //获取原对象的 class方法
    Method classMethod = class_getInstanceMethod(originClass, @selector(class));
    const char *types = method_getTypeEncoding(classMethod);
    //给新类添加 class 方法    "v@:@"
    class_addMethod(newClassCreate, @selector(class), (IMP)kvo_class, types);
    
    //注册类
    objc_registerClassPair(newClassCreate);
    return newClassCreate;
}

static Class kvo_class(id self,SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}
有没有setter方法
- (BOOL)hasSelector:(SEL)selector {
    Class class = object_getClass(self);
    unsigned int count;
    Method *methodList = class_copyMethodList(class, &count);
    for (unsigned int i = 0; i < count; i++) {
        SEL currSelector = method_getName(methodList[i]);
        if (currSelector == selector) {
            free(methodList);
            return YES;
        }
    }
    free(methodList);
    return NO;
}
static void kvo_setter(id self, SEL _cmd, id newValue) {
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);//获取getter字符串
    if (!getterName) {
        //get name 找不到
    }
    //获取旧值
    id oldValue = [self valueForKey:getterName];
    //调用原类的setter方法
    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    //调用super的 setXXX: 方法
    // 这里需要做个类型强转, 否则会报too many argument的错误
    ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClass, _cmd, newValue);
    
    NSMutableArray *arr = objc_getAssociatedObject(self, &kTTKVOObserver);
    for (TTObserverInfo *info in arr) {
        if ([info.keyPath isEqualToString:getterName]) {
            //异步调用
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                info.observerBlock(info.observer,getterName,oldValue,newValue);
            });
 
        }
    }
}


static NSString * getterForSetter(NSString *setter) {
    if (setter.length <= 0 || ![setter containsString:@"set"]) {
        return nil;
    }
    NSString *subStr = [[setter substringFromIndex:3] lowercaseString];
    NSString *getter = [subStr substringToIndex:subStr.length-1];
    return getter;
}

详细说明:

第一步: 通过 setterWithGetter 根据keyPath来获取 setter 的名字(SEL),就是要把 key 变成 setKey: ,再使用 class_getInstanceMethod 方法获取 Method ,没有就抛出异常。

第二步:查看类是否包含自己创建的前缀,没有就是还没有创建自己的类,然后创建自己的类,使用object_setClass把对象的isa指针指向自己所创建的类。

动态创建一个类使用了 runtime 的以下函数:

创建一个类
参数1:父类 参数2:新类名 参数3:size 传0 
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes)

返回一个新类,并重写了 class 方法,隐瞒了你没有对原对象做处理。

第三步: 检查创建的类有没有 setKey: 方法,调用 hasSelector方法,这时候对象的类已经变成我们自己的类了,去methodList检查有没有 setKey: ,没有就创建setter方法并添加,然后通知每个观察者(调用传入的block)。

第四步:把创建的观察者添加到array里。使用setAssociation绑定对象,使新对象持有这个观察者数组。

移除通知
- (void)tt_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    NSMutableArray *arr = objc_getAssociatedObject(self, &kTTKVOObserver);
    if (!arr) {
        return;
    }
    TTObserverInfo *inf = nil;
    for (TTObserverInfo *info in arr) {
        if ([info.keyPath isEqualToString:keyPath] && info.observer == observer) {
            inf = info;
            break;
        }
    }
    [arr removeObject:inf];
}
监听者TTObserverInfo

只是使用这个类保存了监听者的一些信息及block回调。

@interface TTObserverInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, strong) TTObserverBlock observerBlock;
@end

@implementation TTObserverInfo

- (instancetype)initWithObserver:(NSObject *)observer keyPath:(NSString *)keyPath observerBlock:(TTObserverBlock)block {
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _observerBlock = block;
    }
    return self;
}

@end

以上就是KVO的基本原理及通过 runtime 自己实现一个KVO的所有内容。

待解决:

1、自定义实现的KVO监听基本数据类型会导致崩溃,原因是系统KVO监听基本数据类型的时候,会自动转成 NSNumber 对象类型,但是自己写的没有做处理。有没有更好的方法,在某一时机判断如果是基本数据类型 转成 对象类型,在调用block回调的时候再转回去。


参考

如何自己动手实现 KVO

Key-Value Observing Programming Guide

demo地址

上一篇下一篇

猜你喜欢

热点阅读