阅读 217

iOS 底层原理:KVO原理探索&自定义KVO

一、KVO 细节分析

context 的使用

打开 苹果官方文档,查看context相关定义:

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.

The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the balance and interestRate properties chosen this way.

注册方法addObserver:forKeyPath:options:context:中的context可以传入任意数据,并且可以在监听方法中接收到这个数据。

  • context作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。

    • KVO只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:,我们通常情况下可以在注册方法中指定contextNULL,并在监听方法中通过objectkeyPath来判断触发KVO的来源。
    • 但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对 account 对象的 balance 属性进行观察。问题:
      • 当 balance 发生改变时,应该由谁来处理呢?
      • 如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?
    • 这时候通过使用context就可以很好地解决这个问题,在注册方法中为context设置一个独一无二的值,然后在监听方法中对context值进行检验即可。
  • 苹果的推荐用法:用context来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为context的值。可以为整个类设置一个context,然后在监听方法中通过objectkeyPath来确定被观察属性,这样存在继承的情况就可以通过context来判断;也可以为每个被观察对象属性设置不同的context,这样使用context就可以精确的确定被观察对象属性。

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    复制代码

    - (void)registerAsObserverForAccount:(Account*)account {
        [account addObserver:self
                  forKeyPath:@"balance"
                     options:(NSKeyValueObservingOptionNew |
                              NSKeyValueObservingOptionOld)
                     context:PersonAccountBalanceContext];
    
        [account addObserver:self
                  forKeyPath:@"interestRate"
                     options:(NSKeyValueObservingOptionNew |
                              NSKeyValueObservingOptionOld)
                      context:PersonAccountInterestRateContext];
    }
    复制代码

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
    
        if (context == PersonAccountBalanceContext) {
            // Do something with the balance…
    
        } else if (context == PersonAccountInterestRateContext) {
            // Do something with the interest rate…
    
        } else {
            // Any unrecognized context must belong to super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                   context:context];
        }
    }
    复制代码
  • context优点:嵌套少、性能高、更安全、扩展性强。

  • context注意点:

    • 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash
    • 空传NULL而不应该传nil

移除观察者

对于观察者,网上很多人说iOS9以后就不用移除了,这样说对吗,我们打开 苹果官方文档 ,可以找到下面这段内容:

An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.

  • 简单解释一下就是:当观察者被释放时它并不会主动移除自己。观察对象还是会继续发送通知,对已经释放的内存发送消息,会抛出异常导致Crash。因此观察者要在释放前移除它们自己,下面用示例来说明。

创建SSLDetailViewController类:

@interface SSLDetailViewController ()
@property (nonatomic, strong) SSLStudent *student;
@end

@implementation SSLDetailViewController

// student 为上一个页面的属性
- (instancetype)initWithStudent:(SSLStudent *)student
{
    if (self = [super init]) {
        self.student = student;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    
    //  weak observer
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"SSLDetailViewController :%@",change);
}

- (void)dealloc {}

@end
复制代码
  • student是上一个页面的属性传值过来的,作为观察对象。
  • SSLDetailViewController是观察者,dealloc中没有操作。
  • touchesBegan改变name,出发KVO

push进入SSLDetailViewController页面,然后页面中touchesBegan查看结果:

2021-07-28 17:38:06.665226+0800 KVO [34046:504715] SSLDetailViewController :{
    kind = 1;
    new = "hello word";
}
复制代码

pop离开页面,然后再次push进入SSLDetailViewController页面,这时的student依然通过上一个页面的属性传值过来,页面中touchesBegan程序报错:

image.png

  • 所以要按照苹果官方推荐的方式来做:在观察者初始化期间(init或者viewDidLoad的时候)注册为观察者,在释放过程中(dealloc时)调用移除方法,这样可以保证它们是成对出现的,是一种比较理想的使用方式。

KVO 的自动触发

可以在被观察对象的类中重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法来控制KVO的自动触发。

如果我们只允许外界观察SSLStudentname属性,可以在SSLStudent类如下操作。这样外界就只能观察name 属性,即使外界注册了对SSLStudent对象其它属性的监听,那么在属性发生改变时也不会触发KVO

// 返回值代表允不允许触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"name"]) {
        automatic = YES;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}
复制代码

KVO 的手动触发

为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。

可以通过在为成员变量赋值的前后手动调用willChangeValueForKey:didChangeValueForKey:两个方法来手动触发KVO,示例:

+ (BOOL)automaticallyNotifiesObserversOfName
{
    return NO;
}
复制代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.student willChangeValueForKey:@"age"];
    self.student->_age = 18;
    [self.student didChangeValueForKey:@"age"];
}
复制代码
  • NSKeyValueObservingOptionPrior(分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在willChangeValueForKey:didChangeValueForKey:的时候进行的。
  • 如果注册方法中options传入NSKeyValueObservingOptionPrior,那么可以通过只调用willChangeValueForKey:来触发改变前的那次KVO,可以用于在属性值即将更改前做一些操作。

 一对多关系

有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。

比如我们想要对Download类中的downloadProgress属性进行KVO监听,该属性的改变依赖于writtenDatatotalData属性的改变。观察者监听downloadProgress,当writtenDatatotalData属性值改变时,观察者也应该被通知。

重写以下方法来指明downloadProgress属性依赖于writtenDatatotalData

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"writtenData",@"totalData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (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];
}
复制代码

SSLDetailViewController中的调用代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.student addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.writtenData += 10;
    self.person.totalData  += 1;
}
复制代码

对可变数组的观察

KVO可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVCmutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArrayNSSet
(注意:如果直接对集合对象进行操作改变,不会触发KVO。)

示例代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.student addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.student mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    [[self.student mutableArrayValueForKey:@"dateArray"] removeObject:@"1"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
复制代码

查看打印结果:

2021-07-29 14:44:31.606480+0800 001---KVO[15464:280688] {
    indexes = "<_NSCachedIndexSet: 0x600000b68f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        1
    );
}
2021-07-29 14:44:31.606802+0800 001---KVO[15464:280688] {
    indexes = "<_NSCachedIndexSet: 0x600000b68f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 3;
}
复制代码
  • 关于kind的值:
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,
        NSKeyValueChangeInsertion = 2,
        NSKeyValueChangeRemoval = 3,
        NSKeyValueChangeReplacement = 4,
    };
    复制代码

二、KVO 原理分析

KVO实现 官方文档介绍

我们先打开苹果官方文档 看一下它是怎么介绍KVO的实现的:

Key-Value Observing Implementation Details
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.

  • 这里提到KVO的实现用到了isa-swizzling技术,isa会指向新的类,接下来用示例进行探索。

KVO 动态生成子类

断点调试,在KVO调用前通过object_getClassName获取self.person类名:

image.png

  • 这时self.person的类名是SSLPerson没有问题。

KVO调用后通过object_getClassName获取self.person类名:

image.png

  • 这时self.person的类名变成了NSKVONotifying_SSLPerson,那么它和SSLPerson是什么关系呢。

创建测试代码如下,在KVO调用前打印SSLPerson的子类,在KVO调用后打印SSLPersonNSKVONotifying_SSLPerson的子类

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[SSLPerson alloc] init];
    [self printClasses:[SSLPerson class]];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClasses:[SSLPerson class]];
    [self printClasses:objc_getClass("NSKVONotifying_SSLPerson")];
}

// 遍历类以及子类
- (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);
}
复制代码

查看打印结果:

2021-07-29 16:10:06.844021+0800 002---KVO原理探讨[18966:358659] classes = (
    SSLPerson,
    SSLStudent
)
2021-07-29 16:10:06.847060+0800 002---KVO原理探讨[18966:358659] classes = (
    SSLPerson,
    "NSKVONotifying_SSLPerson",
    SSLStudent
)
2021-07-29 16:10:06.849750+0800 002---KVO原理探讨[18966:358659] classes = (
    "NSKVONotifying_SSLPerson"
)
复制代码
  • 可以看到新创建的NSKVONotifying_SSLPersonSSLPerson的子类,而且自己没有子类。

接下里我们看一下,当调用removeObserver后都做了什么。
断点进入ViewControllerdealloc,打印查看结果:

image.png

  1. self.personisa又重新指回了SSLPerson
  2. NSKVONotifying_SSLPerson并没有销毁,成为了SSLStuent的子类。

KVO 动态生成的子类都有哪儿些方法

1. 查看所有方法

创建代码如下,打印NSKVONotifying_SSLPerson类的所有方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[SSLPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClassAllMethod:objc_getClass("NSKVONotifying_SSLPerson")];
}

// 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    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);
}
复制代码

查看打印结果:

setNickName:-0x7fff207bf03f
class-0x7fff207bdb49
dealloc-0x7fff207bd8f7
_isKVOA-0x7fff207bd8ef
复制代码
  • 这些能打印出来的方法,都是NSKVONotifying_SSLPerson重写的。
  • _isKVOA就是判断当前的类是不是KVO创建的。
  • dealloc中实例变量的isa重新指回到原来的SSLPerson类。

2. class方法重写

断点调试:

image.png

  • 重写的class方法内,返回的是NSKVONotifying_SSLPerson的父类SSLPerson

3. setter方法重写

看示例代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[SSLPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nickName = @"SSL";
    self.person->name    = @"Spring";
}

// KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
复制代码

点击屏幕,查看打印结果:

2021-07-29 17:49:08.827957+0800 002---KVO原理探索[22666:436552] {
    kind = 1;
    new = SSL;
}
复制代码
  • 可以看到成员变量name并没有被触发,nickName属性被触发了,说明在setNickName方法中做了KVO的相关操作。

如下图,当self.personisa重新指回SSLPerson后,SSL可以被成功打印,说明setNickName内是调用了父类的实现的。

image.png

三、自定义 KVO

自定义 KVO 基本功能

创建NSObject+SSLKVO分类,仿照KVO,实现相关功能。

NSObject+SSLKVO.h

@interface NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
复制代码

NSObject+SSLKVO.M

static NSString *const kSSLKVOPrefix = @"SSLKVONotifying_";
static NSString *const kSSLKVOAssiociateKey = @"kSSLKVO_AssiociateKey";

@implementation NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : SSLKVONotifying_SSLPerson
    object_setClass(self, newClass);
    // 4: 保存观察者
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    // 指回给父类
    Class superClass = [self class];
    object_setClass(self, superClass);
}

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
    }
}

#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kSSLKVOPrefix,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的指向是SSLPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)ssl_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)ssl_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)ssl_dealloc, deallocTypes);
    
    return newClass;
}

static void ssl_dealloc(id self,SEL _cmd){
    Class superClass = [self class];
    object_setClass(self, superClass);
}

static void ssl_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    
    void (*ssl_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)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    ssl_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 既然观察到了,下一步不就是回调 -- 让我们的观察者调用
    // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: 拿到观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    
    // 2: 消息发送给观察者
    SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
    
}

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

#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];
}

@end
复制代码

自定义 KVO 进阶

上面的代码,已经可以实现KVO的基本功能,但是不能监听多个属性,接下来进行相关的优化。

优化思路:创建SSLKVOInfo类保存观察者信息,将多个SSLKVOInfo实例添加到数组中,再通过关联对象保存到self中,进行相关的操作。

SSLKVOInfo类:

typedef NS_OPTIONS(NSUInteger, LGKeyValueObservingOptions) {

    LGKeyValueObservingOptionNew = 0x01,
    LGKeyValueObservingOptionOld = 0x02,
};

@interface SSLKVOInfo : NSObject
@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString    *keyPath;
@property (nonatomic, assign) LGKeyValueObservingOptions options;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options;
@end


@implementation SSLKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options{
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.options  = options;
    }
    return self;
}
@end
复制代码

NSObject+SSLKVO.m中优化的相关方法:

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : SSLKVONotifying_SSLPerson
    object_setClass(self, newClass);
    // 4: 保存观察者信息
    SSLKVOInfo *info = [[SSLKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    
    if (!observerArr) {
        observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

}

- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    
    for (SSLKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }

    if (observerArr.count<=0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

static void ssl_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    
    void (*ssl_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)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    ssl_msgSendSuper(&superStruct,_cmd,newValue);
    // 1: 拿到观察者
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    
    for (SSLKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 对新旧值进行处理
                if (info.options & LGKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & LGKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                // 2: 消息发送给观察者
                SEL observerSEL = @selector(ssl_observeValueForKeyPath:ofObject:change:context:);
                objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
            });
        }
    }
    
}
复制代码

自定义 KVO 函数式

通过ssl_observeValueForKeyPath:ofObject:change:context:这种调用方式很不方便,我们引入函数式思想,通过block来实现监听的回调,接下来看一下类的改造。

SSLKVOInfo类:

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

@implementation SSLKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(SSLKVOBlock)block{
    if (self=[super init]) {
        _observer = observer;
        _keyPath  = keyPath;
        _handleBlock = block;
    }
    return self;
}
@end
复制代码

NSObject+SSLKVO.h

typedef void(^SSLKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(SSLKVOBlock)block;
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
复制代码

NSObject+SSLKVO.m

static NSString *const kSSLKVOPrefix = @"SSLKVONotifying_";
static NSString *const kSSLKVOAssiociateKey = @"kSSLKVO_AssiociateKey";

@implementation NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(SSLKVOBlock)block{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : SSLKVONotifying_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)(kSSLKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    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)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    
    if (observerArr.count<=0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
    }
}

#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kSSLKVOPrefix,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)ssl_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)ssl_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)ssl_dealloc, deallocTypes);
    
    return newClass;
}

static void ssl_dealloc(id self,SEL _cmd){
    Class superClass = [self class];
    object_setClass(self, superClass);
    }

static void ssl_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*ssl_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)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    ssl_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 5: 信息数据回调
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    
    for (LGInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

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

#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];
}

@end
复制代码

四、FBKVOController 探索

FBKVOControllerFacebook开源的一个基于系统KVO实现的框架。支持Objective-CSwift语言。
GitHubgithub.com/facebook/KV…

FBKVOController 的优点

  • 会自动移除观察者。
  • 函数式编程,可以一行代码实现系统KVO的三个步骤。
  • 实现KVO与事件发生处的代码上下文相同,不需要跨方法传参数。
  • 增加了blockSEL自定义操作对NSKeyValueObserving回调的处理支持。
  • 每一个keyPath会对应一个block或者SEL,不需要使用if判断keyPath
  • 可以同时对一个对象的多个属性进行监听,写法简洁。
  • 线程安全。

FBKVOController 的使用示例

@interface ViewController ()
@property (nonatomic, strong) FBKVOController *kvoCtrl;
@property (nonatomic, strong) SSLPerson *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[SSLPerson alloc] init];
    self.person.name = @"ssl";
    self.person.age = 18;
    self.person.mArray = [NSMutableArray arrayWithObject:@"1"];

    [self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(ssl_observerAge)];
    [self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****name:%@****",change);
    }];
    [self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****mArray:%@****",change);
    }];
    
}

- (void)ssl_observerAge{
    NSLog(@"来了 改变年级");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"点击屏幕");
    self.person.name = [NSString stringWithFormat:@"%@+",self.person];
    self.person.age ++;
    [[self.person mutableArrayValueForKey:@"mArray"] addObject:self.person.name];
}

#pragma mark - lazy
- (FBKVOController *)kvoCtrl{
    if (!_kvoCtrl) {
        _kvoCtrl = [FBKVOController controllerWithObserver:self];
    }
    return _kvoCtrl;
}

@end
复制代码

FBKVOController 原理

FBKVOController用到了中介者模式,通过创建一个FBKVOController的实例,由它进行所有监听、回调和释放的工作。

1. FBKVOController 是如何监听的

进入observe:keyPath:options:block:方法:

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // observe object with info
  [self _observe:object info:info];
}
复制代码

进入_observe:info:方法:

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] observe:object info:info];
}
复制代码
  • _objectInfosMap类型:
    {
      NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
      pthread_mutex_t _lock;
    }
    复制代码
  • _FBKVOSharedController类是个单例,存储相关信息。

进入observe:info:开始观察:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // register info
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  // add observer
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}
复制代码
  • 这里的objectself.person传过来的,而不是VC,因为这里的回调实现用的是block不需要再回调到VC
  • [_infos addObject:info];相关信息添加到单例中。
  • [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];:这里又调用了KVO的监听操作。

2. FBKVOController 是如何自动remove

VC释放的时候_kvoCtrl作为属性也会释放,就会调用到它的dealloc方法,自动remove也是在这里进行实现。

进入FBKVOControllerdealloc

- (void)`dealloc`:
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

- (void)unobserveAll
{
  [self _unobserveAll];
}
复制代码

进入_unobserveAll

- (void)_unobserveAll
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMapTable *objectInfoMaps = [_objectInfosMap copy];

  // clear table and map
  [_objectInfosMap removeAllObjects];

  // unlock
  pthread_mutex_unlock(&_lock);

  _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];

  for (id object in objectInfoMaps) {
    // unobserve each registered object and infos
    NSSet *infos = [objectInfoMaps objectForKey:object];
    [shareController unobserve:object infos:infos];
  }
}
复制代码
  • 拿到_FBKVOSharedController单例,调用unobserve:infos:

进入unobserve:infos:

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
  if (0 == infos.count) {
    return;
  }

  // unregister info
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

  // remove observer
  for (_FBKVOInfo *info in infos) {
    if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;
  }
}
复制代码
  • 移除相关信息:
    for (_FBKVOInfo *info in infos) {
        [_infos removeObject:info];
    }
    复制代码
  • 进行removeObserver操作:
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    复制代码
文章分类
iOS
文章标签