对KVO的实现浅尝辄止

Tags
iOS
Date
May 29, 2021
isa swizzling 不求甚解
这篇是对isa swizzling的研究之一。
👉
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的黑魔法。一起来看看吧。

观察frame

新建一个Demo,在ViewController里,对self.viewframe进行观察。
- (void)viewDidLoad { [super viewDidLoad]; [self.view addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil]; Class class1 = self.view.class; NSLog(@"class1 %@ %p", class1, class1); Class class2 = object_getClass(self.view); NSLog(@"class2 %@ %p", class2, class2); Class class3 = class_getSuperclass(class2); NSLog(@"class3 %@ %p", class3, class3); unsigned int methodCount = 0; Method *methods = class_copyMethodList(object_getClass(self.view), &methodCount); for (unsigned int methodIndex = 0; methodIndex < methodCount; methodIndex++) { Method method = methods[methodIndex]; NSLog(@"method %@ %p %p", NSStringFromSelector(method_getName(method)), method, method_getImplementation(method)); } free(methods); }
打印结果
class1 UIView 0x7fff804345d8 class2 NSKVONotifying_UIView 0x600003270000 class3 UIView 0x7fff804345d8 method setFrame: 0x60000003a788 0x7fff207c0418 method class 0x60000003a708 0x7fff207bdb49 method dealloc 0x60000003a6c8 0x7fff207bd8f7 method _isKVOA 0x60000003a628 0x7fff207bd8ef
初步结论
  1. 在addObserver的时候生成了一个新的子类NSKVONotifying_xxx
  1. 在这个子类里添加对应的setter方法
  1. hook了class方法返回父类的class
  1. 添加了自己的dealloc方法
  1. 添加了一个私有的_isKVOA的方法

移除观察frame

[self.view removeObserver:self forKeyPath:@"frame"];
同样打印类信息
class1 UIView 0x7fff804345d8 class2 UIView 0x7fff804345d8 class3 UIResponder 0x7fff804203d0
此时的实例已经恢复正常。

观察bounds

[self.view addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionNew context:nil];
同样打印输出相关信息
class1 UIView 0x7fff804345d8 class2 NSKVONotifying_UIView 0x600003270000 class3 UIView 0x7fff804345d8 method setBounds: 0x6000000360a8 0x7fff207c0418 method setFrame: 0x60000003a788 0x7fff207c0418 method class 0x60000003a708 0x7fff207bdb49 method dealloc 0x60000003a6c8 0x7fff207bd8f7 method _isKVOA 0x60000003a628 0x7fff207bd8ef
此时对比指针地址,可以发现,除了setBounds是新增的重写方法,其他信息都是重用刚刚观察frame时创建的,也就是说明刚刚创建了并没有销毁。这也可以理解,毕竟这些类信息也不占多少空间,在运行过程来回切换场景addObserverremoveObserver在所难免,用小小空间换时间也是值得的。

移除观察bounds

跟刚刚移除观察frame的的结果一样。
 

小结

大概窥探了一下KVO的前后过程,没有太关注具体的实现细节,成功完成了一篇水文。
 
但是,这已经或即将成为其他hook方案无可避免需要兼容的问题,或者说,是使用者需要考虑的问题。
 
比如Aspect的方案就坦言
👉
KVO works if observers are created after your calls aspect_hookSelector: It most likely will crash the other way around. Still looking for workarounds here - any help appreciated.
 

Loading Comments...