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

相关文章
相关标签/搜索