iOS KVO (验证Object-C实现流程)

前言

在Object-C中有一种观察者模式,即是Key-Value-Observing(KVO)。
利用KVO可以很容易实现视图组件和数据模型的分离。当数据模型的值改变时,会马上触发视图组件,更新视图组件。
在Objc中要实现KVO,必须实现NSKeyValueObServing协议,所幸的是NSObject已经实现该协议,也就是说,几乎所有的Objc对象都可以使用KVO。
本质是:

  • 重写set方法
  • set方法内部会调用willChangeValueForKey
  • set方法内部会调用didChangeValueForKey
一般使用
  1. 被监听者通过 addObserver:forKeyPath:options:context: 方法,添加监听
  2. 监听者重写 observeValueForKeyPath:ofObject:change:context: 方法,实现监听
  3. 被监听者移除监听

实现流程图

KVO 的整个实现基本就是这样的,接下来我们来验证这一系列的东西。

  1. Runtime有没有创建NSKVONotifying_DataModel这个类
  2. NSKVONotifying_DataModel有没有重写这个set方法
  3. NSKVONotifying_DataModel重写set方法做了些什么
  4. 怎么知道set 方法内部调用了_NSSetObjectValueAndNotify这个方法
  5. _NSSetObjectValueAndNotify这个方法内部又做了写什么

当我们对某一个对象的属性监听时,

- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataModel1 = [DataModel new];
    self.dataModel1.name = @"like";
    self.dataModel2 = [DataModel new];
    self.dataModel2.name = @"like";
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.dataModel1 addObserver:self forKeyPath:@"name" options:options context:nil];
}

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

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

我们创建两个实例对象self.dataModel1 self.dataModel2 但是只监听了self.dataModel1.name。当触发-touchesBegan方法时改变name的值,对应就会触发-observeValueForKeyPath。当我们把断点设置到NSLog(@"----%@",change); 用LLDB查看一下各个对象的指针是什么情况。

(lldb) p self.dataModel1.isa
(Class) $4 = NSKVONotifying_DataModel
  Fix-it applied, fixed expression was: 
    self.dataModel1->isa
(lldb) p self.dataModel2.isa
(Class) $5 = DataModel
  Fix-it applied, fixed expression was: 
    self.dataModel2->isa
(lldb) 

这里会发现self.dataModel1的isa 指向的并不是DataModel,而是指向一个新的类NSKVONotifying_DataModel

self.dataModel2的isa 指向的是DataModel。对于self.dataModel2的isa 我们感觉就是应该这样的。但是对于 self.dataModel1 还是有疑问的。因为我们项目中并没有NSKVONotifying_DataModel这个类,现在却指向它。

图1中示例图说是Runtime 创建了一个对开发者而言的隐藏类。因为是使用了KVO 之后才有这个类的NSKVONotifying_DataModel,假设它的父类是DataModel。稍后我们可以Runtime 来验证一下它的父类。

我们不妨在Xcode中手动创建一个NSKVONotifying_DataModel类。看看是什么情况

2018-09-22 14:37:48.885883+0800 Test_KVO[13678:432482]
[general] KVO failed to allocate class pair for name NSKVONotifying_DataModel, 
automatic key-value observing will not work for this class

这就说明NSKVONotifying_DataModel没有成功,因为Runtime 在运行时就帮我们创建了一个。

这验证了Runtime 确实创建了一个NSKVONotifying_DataModel

我们也不妨用Runtime查看一下这个两个类内部的方法都是什么

- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataModel1 = [DataModel new];
    self.dataModel1.name = @"like";
    self.dataModel2 = [DataModel new];
    self.dataModel2.name = @"like";
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.dataModel1 addObserver:self forKeyPath:@"name" options:options context:nil];
    
    NSLog(@"0----%s",object_getClassName(self.dataModel1));
    [self getMethodWithClass:object_getClass(self.dataModel1)];
    
    NSLog(@"0----%s",object_getClassName(self.dataModel2));
    [self getMethodWithClass:object_getClass(self.dataModel2)];
    
    NSLog(@"%@ %@",[self.dataModel1 class],[self.dataModel2 class]);
}

- (void)getMethodWithClass:(Class)cls {
    unsigned  int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString * methodStr = [NSMutableString string];
    for (int i =0 ; i< count; i++) {
        Method method = methods[i];
        NSString * str =  NSStringFromSelector(method_getName(method));
        [methodStr appendString:str];
        [methodStr appendString:@" "];
    }
    free(methods);
    NSLog(@"方法--%@",methodStr);
}

打印的结果

 Test_KVO[15009:524851] 0----NSKVONotifying_DataModel
 Test_KVO[15009:524851] 方法--setName: class dealloc _isKVOA 
 Test_KVO[15009:524851] 0----DataModel
 Test_KVO[15009:524851] 方法--.cxx_destruct name setName: 
 Test_KVO[15009:524851] DataModel DataModel

从这个结果也能说明一个问题NSKVONotifying_DataModel的父类应该是DataModel

DataModelNSKVONotifying_DataModel
setName:setName:
namedealloc
.cxx_destruct_isKVOA
class

可以看出来NSKVONotifying_DataModel 不但重写了属性的set方法,还重写了
-class -dealloc还新加了一个-_isKVOA方法。

NSKVONotifying_DataModel
关于重写 class方法

我们去调用[self.dataModel1 class] 发现打印的还是
DataModel 这个类,可能内部返回就是DataModel类,是为了不让外界知道有NSKVONotifying_DataModel 这个类的存在。

_isKVOA 这个方法可能是个BOOL 值,返回YES说明这个类是一个Runtime自动生成的类。
dealloc 可能就是做一些其他的事情,销毁自己的时候把一些方法移除掉。

DataModel

.cxx_destruct ARC下对象实例变量的释放过程在 .cxx_destruct内完成 ,详细可以查看

剩下的类就是自己添加的和属性的get set 方法

到现在为止我们已经验证了 Runtime这个过程的操作,但是我们怎么知道重写set 方法内部做了些什么呢?

其实我们还要用到强大的Runtime 还了解一下

- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataModel1 = [DataModel new];
    self.dataModel1.name = @"like";
    self.dataModel2 = [DataModel new];
    self.dataModel2.name = @"like";
    
    NSLog(@"添加KVO前的方法\n %p--%p",
          [self.dataModel1 methodForSelector:@selector(setName:)],
          [self.dataModel2 methodForSelector:@selector(setName:)]);
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.dataModel1 addObserver:self forKeyPath:@"name" options:options context:nil];
   
    NSLog(@"添加KVO后的方法\n %p--%p",
          [self.dataModel1 methodForSelector:@selector(setName:)],
          [self.dataModel2 methodForSelector:@selector(setName:)]);
    NSLog(@"添加");
   
    }

打印结果如下:

Test_KVO[15415:562521] 添加KVO前的方法
 0x10717b430--0x10717b430
Test_KVO[15415:562521] 添加KVO后的方法
 0x1074bda7a--0x10717b430
 // 断点下 进行调试
(lldb) p (IMP)0x10717b430
(IMP) $0 = 0x000000010717b430 (Test_KVO`-[DataModel setName:] at DataModel.h:12)
(lldb) p (IMP)0x1074bda7a
(IMP) $1 = 0x00000001074bda7a (Foundation`_NSSetObjectValueAndNotify)
(lldb) 

由此结果可以看出,添加KVO前后set方法的指针已经变了,这里也说明set方法被重写了。

这验证了NSKVONotifying_DataModel 确实重写了set 方法

我们用IMP 可以看到各自内部的set方法。我们关注NSKVONotifying_DataModel这个内部的实现是调用Foundation 框架中的_NSSetObjectValueAndNotify的C语言方法

但是我们是如何知道_NSSetObjectValueAndNotify内部是怎么样的呢?

这里需要使用逆向开发了,通过越狱手机可以获取Foundation框架,使用Hopper来解析源码生成的是汇编语言,看看汇编源码会发现_NSSetObjectValueAndNotify内部注释有提示说调用了didChangeValueForKey

那我们需要怎么验证这个问题呢?

  • 我们需要手动触发一个KVO

言外之意我们不触发set 方法,用_name = @'LiMing'可以不触发set 方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.dataModel1 willChangeValueForKey:@"name"];
    [self.dataModel1 didChangeValueForKey:@"name"];
}

我们直接调用这连个方法发现确实触发了KVO-(void)observeValueForKeyPath方法。
这里还验证了另一个问题如果我们直接调用didChangeValueForKey会发现不能触发KVO,如果前面加上willChangeValueForKey就可以触发KVO,说明调用didChangeValueForKey的时候会检测 是否调用了willChangeValueForKey,可见OC内部代码的严谨性。

这验证了_NSSetObjectValueAndNotify 的一些内部操作

总结

  1. 添加属性的KVO 会触发Runtime 创建一个NSKVONotifying_XXX的内部隐藏类
  2. NSKVONotifying_XXX的set 方法会调用Foundation_NSSetObjectValueAndNotify方法
  3. _NSSetObjectValueAndNotify内部会调用
  • willChangeValueForKey
  • [ super setName:]
  • didChangeValueForKey
  1. didChangeValueForKey会发消息给KVOobserveValueForKeyPath方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值